您当前的位置:首页 > 电脑百科 > 程序开发 > 移动端 > Android

安卓漏洞 CVE 2017-13287 复现详解

时间:2020-02-16 10:36:44  来源:  作者:

安卓漏洞 CVE 2017-13287 复现详解

 

 

2018年4月,Android安全公告公布了CVE-2017-13287漏洞。

 

与同期披露的其他漏洞一起,同属于框架中Parcelable对象的写入(序列化)与读出(反序列化)的不一致所造成的漏洞。

 

在刚看到谷歌对于漏洞给出的补丁时一头雾水,

 

在这里要感谢heeeeen@MS509Team在这个问题上的成果,启发了我的进一步研究。

 

原理

 

谷歌在Android中提供了Parcelable作为高效的序列化实现,用来支持IPC调用中多样的对象传递需求。

但是序列化和反序列化的过程依旧依靠程序员编写的代码进行同步。

 

那么当不同步的时候,漏洞就产生了。

 

Bundle

传输的时候Parcelable对象按照键值对的形式存储在Bundle内,Bundle内部有一个ArrayMap用hash表进行管理。

 

反序列化过程如下:

    /* package */ void unparcel() {        synchronized (this) {           final Parcel parcelledData = mParcelledData;           int N = parcelledData.readInt();           if (N < 0) {               return;           }           ArrayMap<String, Object> map = mMap;           try {               parcelledData.readArrayMapInternal(map, N, mClassLoader);           } catch (BadParcelableException e) {           } finally {               mMap = map;               parcelledData.recycle();               mParcelledData = null;           }        }    }

首先读取一个int指示里面有多少对键值对。

    /* package */ void readArrayMapInternal(ArrayMap outVal, int N,        ClassLoader loader) {        if (DEBUG_ARRAY_MAP) {           RuntimeException here =  new RuntimeException("here");           here.fillInStackTrace();           Log.d(TAG, "Reading " + N + " ArrayMap entries", here);        }        int startPos;        while (N > 0) {           if (DEBUG_ARRAY_MAP) startPos = dataPosition();           String key = readString();           Object value = readValue(loader);           outVal.Append(key, value);           N--;        }        outVal.validate();    }

之后的每一对先是Key的字符串,然后是对应的Value。

    public final Object readValue(ClassLoader loader) {        int type = readInt();        switch (type) {        case VAL_NULL:           return null;        case VAL_STRING:           return readString();        case VAL_INTEGER:           return readInt();        case VAL_MAP:           return readHashMap(loader);        case VAL_PARCELABLE:           return readParcelable(loader);        case VAL_SHORT:           return (short) readInt();        case VAL_LONG:           return readLong();

值内部先是一个int指示值的类型,再存储实际值。

当Bundle被写入Parcel时:

    void writeToParcelInner(Parcel parcel, int flags) {        final ArrayMap<String, Object> map;        synchronized (this) {           if (mParcelledData != null) {               if (mParcelledData == NoImagePreloadHolder.EMPTY_PARCEL) {                  parcel.writeInt(0);               } else {                  int length = mParcelledData.dataSize();                  parcel.writeInt(length);                  parcel.writeInt(BUNDLE_MAGIC);                  parcel.appendFrom(mParcelledData, 0, length);               }               return;           }           map = mMap;        }    }

先写入Bundle总共的字节数,再写入魔数,之后是指示键值对数的N,还有相应的键值对。

 

LaunchAnyWhere

 

弄明白Bundle的内部结构后,先来看看漏洞触发的地方:

安卓漏洞 CVE 2017-13287 复现详解

 

这个流程是AppA在请求添加一个帐号:

  1. AppA请求添加一个帐号
  2. System_server接受到请求,找到可以提供帐号服务的AppB,并发起请求
  3. AppB返回了一个Bundle给系统,系统把Bundle转发给AppA
  4. AccountManagerResponse在AppA的进程空间中调用startActivity(intent)调起一个Activity。

 

在第4步中,如果AppA的权限较高,比如Settings,那么AppA可以调用正常App无法调用的未导出Activity。

 

并且在第3步中,AppB提供的Bundle在system_server端被反序列化,之后system_server根据之前得到的内容再序列化并传递给AppA。

 

那么如果对应的传递内容的序列化和反序列化代码不一样,就会影响到自己以及之后的内容的结果。

 

传递的Bundle对象中包含一个重要键值对{KEY_INTENT:intent},指定了AppA稍后调用的Activity。

 

如果这个被指定成Setting中的com.android.settings.password.ChooseLockPassword,就可以在不需要原本锁屏密码的情况下重新设置锁屏密码。

 

谷歌在这个过程中进行了检查,保证Intent中包含的Activity所属的签名和AppB一致,并且不是未导出的系统Actiivity。

protected void checkKeyIntent(int authUid, Intent intent) throws SecurityException {    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 (!isExportedSystemActivity(targetActivityInfo)            && (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);    }}

攻击思路便是在system_server进行检查时Bundle中的恶意{KEY_INTENT:intent}看不到,但是在重新序列化之后在Setting出现,这样就绕过了检查。

 

利用

 

首先来看看漏洞所在的代码

    public static final Parcelable.Creator<VerifyCredentialResponse> CREATOR            = new Parcelable.Creator<VerifyCredentialResponse>() {        @Override        public VerifyCredentialResponse createFromParcel(Parcel source) {            int responseCode = source.readInt();            VerifyCredentialResponse response = new VerifyCredentialResponse(responseCode, 0, null);            if (responseCode == RESPONSE_RETRY) {                response.setTimeout(source.readInt());            } else if (responseCode == RESPONSE_OK) {                int size = source.readInt();                if (size > 0) {                    byte[] payload = new byte[size];                    source.readByteArray(payload);                    response.setPayload(payload);                }            }            return response;        }        @Override        public VerifyCredentialResponse[] newArray(int size) {            return new VerifyCredentialResponse[size];        }    };    @Override    public void writeToParcel(Parcel dest, int flags) {        dest.writeInt(mResponseCode);        if (mResponseCode == RESPONSE_RETRY) {            dest.writeInt(mTimeout);        } else if (mResponseCode == RESPONSE_OK) {            if (mPayload != null) {                dest.writeInt(mPayload.length);                dest.writeByteArray(mPayload);            }        }    }

仔细阅读,会发现在mResponseCode为RESPONSE_OK时,

 

如果mPayload为null,那么writeToParcel不会在末尾写入0来正确的指示Payload部分的长度。

 

而在createFromParcel中是需要readInt来获知的,这个就带来了序列化与反序列化过程的不一致。

 

可以通过精心构造的payload来绕过检查。

 

难点在于和已经有人公开过的CVE-2017-13288和CVE-2017-13315不同,

 

它们是重新序列化之后会多出来4个字节。这里是重新序列化之后会少4个字节。

 

安卓漏洞 CVE 2017-13287 复现详解

 

利用String的结构,把恶意intent隐藏在String里。上图每段注释的括号里写了其所占用的字节数。

 

在第一次反序列化时,VerifyCredentialResponse内部的0还在,恶意intent被包装在第二对的Key中。

第二对的值的类型被制定为VAL_NULL,也就是什么都没有,常量值为-1。

 

再次序列化时writeToParcel没有writeInt(0),所以到达Setting的Bundle在RESPONSE_OK之后没有0,原本的String length被视作payload length,调用readByteArray读取。

static jbyteArray android_os_Parcel_createByteArray(JNIEnv* env, jclass clazz, jlong nativePtr)    {    jbyteArray ret = NULL;    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);    if (parcel != NULL) {        int32_t len = parcel->readInt32();        // sanity check the stored length against the true data size        if (len >= 0 && len <= (int32_t)parcel->dataAvail()) {           ret = env->NewByteArray(len);           if (ret != NULL) {               jbyte* a2 = (jbyte*)env->GetPrimitiveArrayCritical(ret, 0);               if (a2) {                  const void* data = parcel->readInplace(len);                  memcpy(a2, data, len);                  env->ReleasePrimitiveArrayCritical(ret, a2, 0);               }           }        }    }    return ret;}

再次调用readInt32读取长度,之后截取数组内容。相应的从Payload length开始的指定长度的内容都被视作payload。

 

只要设置得当,恶意intent就会显露出来成为实质上的第二对键值对。

 

那么之前作为第二对值的VAL_NULL怎么办?之前提过它的常量值是-1,上一对恶意intent刚结束,在这里调用的是readString这个函数。

const char16_t* Parcel::readString16Inplace(size_t* outLen) const{    int32_t size = readInt32();    // watch for potential int overflow from size+1    if (size >= 0 && size < INT32_MAX) {        *outLen = size;        const char16_t* str = (const char16_t*)readInplace((size+1)*sizeof(char16_t));        if (str != NULL) {           return str;        }    }    *outLen = 0;    return NULL;}

再次的readInt32,得到-1,直接返回null,长度为0,会在JNI层中创建一个空字符串返回到JAVA层。那么就是说:VAL_NULL单独作为一个空字符串被读取,之后的三个蓝色块被视作值。

 

这里因为之后的字符串是123456,所以string_length是6.

 

这个很关键,因为在Settings这里被readValue视作type,而6正好是VAL_STRING,也即字符串类型。于是ord('1')= 0x31被视作String length正常使用,正常读取字符串。

 

至此Settings侧正常读取完毕,恶意intent被读取并执行。

 

假String的构造

 

之前略过了包含恶意intent的假String的具体padding过程,这里展开:

 

String_length(4) + Payload_length(4) + PADDING(Size + 16) + EVIL_INTENT(Size) + PADDING(8)String_length = Payload_length = (4 + 4 + Size + 16 + Size + 8) / 2 – 1 = Size + 15

 

这里先给出公式,Size在这里就是Evil_intent部分的长度,String_length和Payload_length在Setting侧都被视作payload的长度使用,故相同。

 

从两个视角去审视这个公式:

 

  1. system_server侧

对于system_server来说,从String_length开始的部分就是单纯的一个字符串,那么它先读取String_length并套用readString16Inplace中的公式。

 

它会从String_length之后读取$ 2(1 + Size + 15)=2Size + 32 $,正好包括总长。

 

  1. Settings侧

对于Settings来说,从Payload_length之后会直接截取对应长度的内容作为数组,即Payload_length之后$Size + 15$,

 

因为Parcel底层的操作对4向上凑整,所以正好露出EVIL_INTENT。

这样就可以达成效果。

 

结果

POC: https://github.com/FXTi/CVE201713287POC

 

总结

 

在IPC这块就算谷歌引入了AIDL这种方式来规定接口,哪怕只是中间所用到的类的序列化过程出现一点失误都会造成如此严重的漏洞。

 

可见安全编程以及代码审计的必要性,没准以后还会有类似机理的漏洞被发掘出来。

 

作者:FXTi

转载自 https://www.anquanke.com/post/id/197710



Tags:安卓漏洞   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
2018年4月,Android安全公告公布了CVE-2017-13287漏洞。 与同期披露的其他漏洞一起,同属于框架中Parcelable对象的写入(序列化)与读出(反序列化)的不一致所造成的漏洞。 在...【详细内容】
2020-02-16  Tags: 安卓漏洞  点击:(85)  评论:(0)  加入收藏
▌简易百科推荐
今天面试遇到同学说做过内存优化,于是我一般都会问那 Bitmap 的像素内存存在哪?大多数同学都回答在 java heap 里面,就比较尴尬,理论上你做内存优化,如果连图片这个内存大户内存...【详细内容】
2021-12-23  像程序那样思考    Tags:Android开发   点击:(8)  评论:(0)  加入收藏
Android logcat日志封装logcat痛点在Android开发中使用logcat非常频繁,logcat能帮我们定位问题,但是在日常使用中发现每次使用都需要传递tag,并且会遇到输出频率很高的log,在多...【详细内容】
2021-12-22  YuCoding    Tags:Android   点击:(8)  评论:(0)  加入收藏
对项目的基本介绍 1.整个框架主要是给MVVM框架使用的,自己写完interface接口后,通过自定义的注解就能自动生成接口方法 2.用Kotlin的Flow去代替Rxjava,因为我发现RxJava功能很...【详细内容】
2021-12-08  网易Leo    Tags:Android开发   点击:(16)  评论:(0)  加入收藏
前言在Android开发过程中,有些时候会根据需要引用别的项目到当前项目里面,而且以Module形式引用。所以本篇博文就来分享一下怎么以Module形式引用别的项目到当前项目中,方便开...【详细内容】
2021-12-07  网易Leo    Tags:Android开发   点击:(22)  评论:(0)  加入收藏
作者:fundroid这篇文章偏阅读一些,大家可以了解下 Android 的一些最新动向。每年9/10月份 Google 都会举行约为期2天的 Android Dev Summit,在活动上 Google 的技术专家们会分...【详细内容】
2021-11-30  像程序那样思考    Tags:Android开发   点击:(15)  评论:(0)  加入收藏
一、 准备工作1、安装JDK,下载地址(可能需要一个oracle账号,大家百度一下或者自行注册一个就行。尽可能选择8或者11,这两个是长期版本)Java SE | Oracle Technology Network | Or...【详细内容】
2021-11-23  永沧    Tags:Android   点击:(28)  评论:(0)  加入收藏
使用Maven Publish Plugin插件。(官方支持)一、在Library的build.gradle中配置plugins { id &#39;com.android.library&#39; id &#39;kotlin-android&#39; id &#39;k...【详细内容】
2021-11-05  羊城小阳    Tags:Android   点击:(37)  评论:(0)  加入收藏
谷歌离推出Play Store应用程序的新数据隐私部分又近了一步。应用程序开发人员现在可以通过谷歌在Play控制台的新 "数据安全表 "填写相关细节。该公司表示,所需信息将从2022年...【详细内容】
2021-10-20    中关村在线  Tags:安卓   点击:(58)  评论:(0)  加入收藏
架构究竟是什么?如何更好的理解架构?我们知道一个APP通常是由class组成,而这些class之间如何组合,相互之间又如何产生作用,就是影响这个APP的关键点。细分的话我们可以将其分为类...【详细内容】
2021-09-17  像程序那样思考    Tags:Android架构   点击:(52)  评论:(0)  加入收藏
概述当Android应用程序需要访问设备上的敏感资源时,应用程序开发人员会使用权限模型。虽然该模型使用起来非常简单,但开发人员在使用权限时容易出错,从而导致安全漏洞。本文中,...【详细内容】
2021-09-07  SecTr安全团队    Tags:Android开发   点击:(66)  评论:(0)  加入收藏
相关文章
    无相关信息
最新更新
栏目热门
栏目头条