分析一个反序列化漏洞, 以及漏洞的产生的利用过程.
本文主要内容:
- 漏洞成因
- 漏洞利用的原理
- 基础知识
- 漏洞利用流程
漏洞成因: #
java.io.ObjectInputStream
类没有对接受的数据进行验证, 导致可以传入一个不可序列化的对象.
利用的原理: #
android.os.binderproxy对象时不可序列化的, 并且它涉及到一个native代码可以将mObject和mOrgue当作一个指针.
具体来说就是android.os.BinderProxy对象在gc时会调用一个native的destory
()函数导致任意代码执行.
native
调用链destroy()--> decStrong() --> refs->mBase->onLastStrongRef(id)
基础知识 #
1. Java反序列化分析 #
每个java object在序列化时都有一个序列号, 这是这种机制称为序列化的原因.
java.io.ObjectOutputStream可以对对象进行序列化, 它的writeObject()方法可以将序列化后写到一个目标输出流中.
java.io.ObjectInputStream可以对对象进行反序列化, 它的readObject()方法可以从目标输入流中读入序列化的对象并将其反序列化为原对象.
下面以实例来进行分析: 我们这里定义了一个Employee类(内部有三个实例域和一个构造函数)(其他内部函数已经省略)
package hihihi;
import java.io.*;
import java.util.*;
public class Employee implements Serializable
{
private String name;
private double salary;
private Date hireDay;
public Employee(String n, double s, int year, int month, int day)
{
name = n;
salary = s;
GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day);
hireDay = calendar.getTime();
}
}
这里是我们的测试程序,运行后会在该程序文件目录下产生一个TestEmployee.db的文件, 这个文件内部的数据即使我们序列化Employee类后产生的序列化数据:
import hihihi.*;
import java.io.*;
import java.util.Date;
public class Test {
public static void main(String[] args) throws Exception {
BinderProxy bT = new BinderProxy();
Employee bE = new Employee("hi", 150, 2017, 3, 9);
Date bD = new Date();
// FileOutputStream fos= new FileOutputStream("TestStream.db");
// ObjectOutputStream os = new ObjectOutputStream(fos);
// os.writeObject(bT);
// os.close();
FileOutputStream fosr = new FileOutputStream("TestEmployee.db");
ObjectOutputStream osr = new ObjectOutputStream(fosr);
osr.writeObject(bE);
osr.close();
ObjectInputStream fisr = new ObjectInputStream(new FileInputStream("TestEmployee.db"));
Object newEmployee = fisr.readObject();
fisr.close();
System.out.println(newEmployee);
// FileOutputStream fosd = new FileOutputStream("TestData.db");
// ObjectOutputStream osd = new ObjectOutputStream(fosd);
// osd.writeObject(bD);
// osd.close();
}
}
简单说一下序列化后的数据:(参看<<java核心技术(卷2)>>
)
每个文件的Magic Number 为: ‘AC ED’
后面紧跟对象序列化的版本号: Ex: ‘00 05’
字符串对象被保存为: 74 2字节表示的字符串长度 字符
当存储一个对象时: 72 2字节的类名长度 类名 8字节长的指纹(serial ID) 1字节长的标志 2
字节长的数据域描述符的数量 数据域描述符 结束标志78 超类类型(没有的话, 为70)
2. Java和Android的GC机制 #
Java对象的生命周期和垃圾回收:(从网上摘录.侵删)
- 创建对象的方式:
- 使用new语句创建对象;
- 使用反射, 调用java.lang.Class或java.lang.reflect.Constructor的newInstance()实例方法;
- 调用对象的clone()方法;
- 使用反序列化手段, 调用java.io.ObjectInputStream对象的readObject()方法.
- 垃圾回收:
对象的可触及性:
- 可触及状态: 当一个对象被创建后, 只要还有引用变量引用该对象, 那么它就始终处于可触及状态;
- 可复活状态: 当程序不再有任何变量引用对象时, 它就进入可复活状态, 该状态的对象, 垃圾回收器会准备释放它的内存, 在释放前, 会调用它的finalize()方法, 这些finlize方法可能使对象重新转到可触及状态;
- 不可触及状态: 当JVM执行完所有的可复活状态的finalize()方法之后, 假设这些方法都没有使对象转到可触及状态. 那么该对象就进入了不可触及状态. 只有对象处于不可触及状态时, 垃圾回收器才会真正回收它占用的内存.
3. Android的Binder机制简介(网上有很多分析Binder机制的文章, 这里就直接给出一个简单的定义); #
binder是Android提供的一种IPC通信机制,方便进程之间交换数据。binder的实现包括一个公共的顶层服务接口,同时实现了这个公共顶层接口的proxy代理端和service端。binder driver充当通信媒介。
4. C++ 对象内存布局: #
C++中, 对象的内存布局为先放父类, 然后放自己的成员, 如果有虚函数, 内存中会有一个放虚函数表的位置在最开始处.
5. 堆喷射: #
原因(为什么要用到堆喷射技术): 控制的rip指针指向的地址是在一个范围内随机变化的, 而我们可以对rip指向的区域进行写操作.
方式: 通过构造特殊的内存布局, 使无论rip指向哪里, 它都会最终
跳转到一个固定的地址处
6. ROP #
在可以控制rip指针后, 我们可以在程序加载的.so中找到一些特定的gadget, 利用内存复写技术, 来控制最终执行system函数(也就是拿到了system权限)
漏洞利用流程 #
1. 触发GC机制 #
BinderProxy的finalize方法调用了native的方法, 会将我们的mOruge处理为指针.
2. 分析native方法: #
————为了我们能够正确的控制指针指针我们期望的地址,而不会让system_server crash掉
destory在native层的调用逻辑:
/Core/jni/android_util_Binder.cpp static void android_os_BinderProxy_destroy(JNIEnv* env, jobject obj)
-----> /libs/utils/RefBase.cpp void RefBase::decStrong(const void* id) const
-----> int32_t android_atomic_dec(volatile int32_t* addr) /system/core/libcutils/atomic.c
-----> /libs/utils/RefBase.cpp void RefBase::onLastStrongRef(const void* /*id*/)
结合下面的代码分析:
传入的object对象即是我们恶意构造的对象, 通过GetIntField()可以获得该对象中的实例域mObject和mOrgue
注意到它将实例域赋值到drl后, 掉了drl->decStrong() 函数—————–drl可控, 即传入的this可控, drl为指向DeathRecipientList的指针
在decStrong函数中
- 首先 refs = mRefs, 即让refs = *(mOruge + 4)
- 调用refs->removeStrongRef(id) 空实现
- c = android_atomic_dec(&refs->mStrong) c = *(mOruge + 4) == 1
- if (c == 1): then …. BLX r2 r2 = * (((*(mOrgue + 4) + 8)) + 12)
这样就完成了对rip的控制, 通过在system_server内存空间的dalvik-heap中进行堆喷射和合理的布置gadget, 就可以完成到system权限的提升.
(具体代码可参照retme的https://github.com/retme7/CVE-2014-7911_poc)
DecStrong的汇编代码
PUSH {R4-R6,LR}
MOV R5, R0
LDR R4, [R0,#4] r4 = *(r0 + 4)
MOV R6, R1
MOV R0, R4
BLX android_atomic_dec
CMP R0, #1
BNE loc_D184
LDR R0, [R4,#8] r0 = *(r4 + 8)
MOV R1, R6
LDR R3, [R0] r3 = *(r0)
LDR R2, [R3,#0xC] r2 = *(r3 + 0xc)
BLX R2
native执行代码:
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();
}
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 (c == 1) {
refs->mBase->onLastStrongRef(id);
if ((refs->mFlags&OBJECT_LIFETIME_MASK) == OBJECT_LIFETIME_STRONG) {
delete this;
}
}
refs->decWeak(id);
}
void RefBase::onLastWeakRef(const void* /*id*/)
{
}