0x01 背景介绍 :
Google在四月份的Android Bulletin上公布了CVE-2016-0846,这是由IMemory组件OOB所导致的任意地址读写,根据bulletin对该漏洞的描述,恶意应用可以利用IMemory本地接口的特权提升漏洞,通过一个本地的恶意应用,可获取系统应用的上下文并执行任意代码。
当然Bulletin上的描述是最坏情况下的后果,笔者能力只够利用该洞进行越界读导致crash。该漏洞作者是Project Zero的James Forshaw,十分珍贵的issue也已公开,且网上也有两个已公开的PoC,分别是Forshaw编写的纯java的PoC以及龚广的cpp编写的PoC,参考资料相当丰富。
0x02 补丁分析 :
该洞的补丁补在libs/binder/IMemory.cpp文件内的BpMemory::getMemory函数中,该函数通过remote()获取一个BpBinder代理对象,并调用该对象的transact函数给运行在远程的服务端发送一个GET_MEMORY请求,并将请求返回的结果通过Parcel类型的reply返回。
然而,由于对远程返回结果的过于信任,未打补丁前,直接将size与offset赋给了成员变量mOffset与mSize,而这样做会存在潜在的威胁,详情请接着看后文的分析。打补丁之后,在赋值前先调用getSize()函数获取mHeap的真正堆大小,然后检验Binder调用传回的offset与size的合法性,从而在IMemory的Proxy端避免了越界读写的可能。
补丁信息如下:
@@ -26,6 +26,7 @@
#include <sys/mman.h>
#include <binder/IMemory.h>
+#include <cutils/log.h>
#include <utils/KeyedVector.h>
#include <utils/threads.h>
#include <utils/Atomic.h>
@@ -187,15 +188,26 @@
if (heap != 0) {
mHeap = interface_cast<IMemoryHeap>(heap);
if (mHeap != 0) {
- mOffset = o;
- mSize = s;
+ size_t heapSize = mHeap->getSize();
+ if (s <= heapSize
+ && o >= 0
+ && (static_cast<size_t>(o) <= heapSize - s)) {
+ mOffset = o;
+ mSize = s;
+ } else {
+ // Hm.
+ android_errorWriteWithInfoLog(0x534e4554,
+ "26877992", -1, NULL, 0);
+ mOffset = 0;
+ mSize = 0;
+ }
}
}
}
}
if (offset) *offset = mOffset;
if (size) *size = mSize;
- return mHeap;
+ return (mSize > 0) ? mHeap : 0;
}
// ---------------------------------------------------------------------------
0x03 类间关系及程序流程分析
如上补丁所补的BpMemory实则是一个实现了IMemory接口的Binder代理对象,通常程序通过该代理向运行在服务端的MemoryBase发送请求。Android系统提供了MemoryHeapBase与MemoryBase两个C++类,以方便应用程序使用匿名共享内存。其中MemoryBase类是在MemoryHeapBase的基础上实现的,在MemoryBase内部有一个类型为IMemoryHeap的强指针mHeap,它指向一个MemoryHeapBase服务,MemoryBase类通过mOffset与mSize来维护mHeap的一小块匿名共享内存。
class MemoryBase : public BnMemory
{
public:
MemoryBase(const sp<IMemoryHeap>& heap, ssize_t offset, size_t size);
virtual ~MemoryBase();
virtual sp<IMemoryHeap> getMemory(ssize_t* offset, size_t* size) const;
protected:
size_t getSize() const { return mSize; }
ssize_t getOffset() const { return mOffset; }
const sp<IMemoryHeap>& getHeap() const { return mHeap; }
private:
size_t mSize;
ssize_t mOffset;
sp<IMemoryHeap> mHeap;
};
IMemory接口定义了MemoryBase服务接口,它主要包括五个成员函数,fasterPointer、pointer、size、offset及getMemory,前四者由IMemory自己实现,其中pointer、size、offset三个函数最终都调用getMemory函数获取返回结果,而getMemory是一个纯虚函数,由IMemory的子类(MemoryBase)来实现。IMemory接口定义如下:
class IMemory : public IInterface
{
public:
DECLARE_META_INTERFACE(Memory);
virtual sp<IMemoryHeap> getMemory(ssize_t* offset=0, size_t* size=0) const = 0;
// helpers
void* fastPointer(const sp<IBinder>& heap, ssize_t offset) const;
void* pointer() const;
size_t size() const;
ssize_t offset() const;
};
BnMemory及BpMemory两个类均实现了IMemory接口,其中BpMemory是一个运行在Client端的Binder代理类,而BnMemory是运行在Server进程中的本地对象类,BnMemory继续派生出MemoryBase,最终由MemoryBase类实现getMemory函数,因而可以理解为,MemoryBase在实例化之后就成了一个Service组件。
IMemory接口通常被media相关的service使用,Android系统允许通过Binder进程间通信机制来传输IMemory对象,通过在Client端调用getMemory,发送一个GET_MEMORY远程请求,即可返回一个IMemoryHeap类型的匿名共享内存缓冲区。当客户端通过pointer()函数获取指向共享内存区域的指针时,pointer函数直接返回heap->base()+offset,而没有对offset进行检查,如果此处offset+base要大于heap的大小,则会造成越界读,那么代码是否在其他地方做了验证?
void* IMemory::pointer() const {
ssize_t offset;
sp<IMemoryHeap> heap = getMemory(&offset);
void* const base = heap!=0 ? heap->base() : MAP_FAILED;
if (base == MAP_FAILED)
return 0;
return static_cast<char*>(base) + offset; <- No check on IMemoryHeap size
}
查看BpMemory::getMemory的代码,发现它直接从reply中读取出了size与offset,并没有对size与offset进行任何合法性的check:
sp<IMemoryHeap> BpMemory::getMemory(ssize_t* offset, size_t* size) const
{
if (mHeap == 0) {
Parcel data, reply;
data.writeInterfaceToken(IMemory::getInterfaceDescriptor());
if (remote()->transact(GET_MEMORY, data, &reply) == NO_ERROR) {
sp<IBinder> heap = reply.readStrongBinder();
ssize_t o = reply.readInt32();
size_t s = reply.readInt32(); <- No check.
if (heap != 0) {
mHeap = interface_cast<IMemoryHeap>(heap);
if (mHeap != 0) {
mOffset = o;
mSize = s;
}
}
}
}
if (offset) *offset = mOffset;
if (size) *size = mSize;
return mHeap;
}
继续跟进,size与offset由remote()->transact(GETMEMORY,data,&reply)的reply中得到,而发送GETMEMORY请求之后,将调用至MemoryBase::onTransact函数,即BnMemory::onTransact函数,其逻辑如下:
status_t BnMemory::onTransact(
uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
{
switch(code) {
case GET_MEMORY: {
CHECK_INTERFACE(IMemory, data, reply);
ssize_t offset;
size_t size;
reply->writeStrongBinder( IInterface::asBinder(getMemory(&offset, &size)) );
reply->writeInt32(offset);
reply->writeInt32(size);
return NO_ERROR;
} break;
default:
return BBinder::onTransact(code, data, reply, flags);
}
}
MemoryBase::getMemory函数逻辑十分简单,直接将MemoryBase的成员变量mOffset、mSize、mHeap返回给调用者,而这三个值均是从MemoryBase对象初始化过程中得到的。
MemoryBase::MemoryBase(const sp<IMemoryHeap>& heap,
ssize_t offset, size_t size)
: mSize(size), mOffset(offset), mHeap(heap)
{
}
sp<IMemoryHeap> MemoryBase::getMemory(ssize_t* offset, size_t* size) const
{
if (offset) *offset = mOffset;
if (size) *size = mSize;
return mHeap;
}
0x04 PoC构造
实际上,在确认IMemory完全信任server端回传的数据后,我们可以伪造一个IMemory Service组件,用它与运行在其他service中的IMemory Client组件进行通信,并利用其他service对我们指定的heap、offset、size的读写操作来达到对service进程的越界读写。
要达到越界读写效果,要求目标service符合以下几个条件:
1. 该service通过Binder IPC接收包含IMemory对象的Parcel,并将IMemory中包含的MemoryHeapBase通过mmap映射到了自己的内存空间。
2. 该service调用IMemory->pointer()函数,并对其所返回的地址进行了读或写操作。
3. 在读写操作中,未对共享内存的边界作合法性检验,或者检验可以被绕过。
在Forshaw与oldfresher的PoC中,他们使用的是运行在Media Player Service中的ICrypto。首先看一看BpCrypto::decrypt()函数,该函数允许用户创建一个IMemory强指针对象,作为sharedBuffer参数传入函数中,同时传入的还有subSample,dstPtr等其他参数,decrypt函数的函数原型如下:
virtual ssize_t decrypt(
bool secure,
const uint8_t key[16],
const uint8_t iv[16],
CryptoPlugin::Mode mode,
const sp<IMemory> &sharedBuffer, size_t offset,
const CryptoPlugin::SubSample *subSamples, size_t numSubSamples,
void *dstPtr,
AString *errorDetailMsg) ;
在decrypt函数中,会将key,iv直接写入Parcel类型的data中,并从subSamples计算出一个sizet类型的totalSize、将dstPtr作为一个uint64t、将sharedMemory作为一个StrongBinder写入data中,并通过remote()->transact(DECRYPT,data,&reply)远程调用,将返回结果写入dstPtr中,如下:
remote()->transact(DECRYPT, data, &reply);
ssize_t result = reply.readInt32();
if (isCryptoError(result)) {
errorDetailMsg->setTo(reply.readCString());
}
if (!secure && result >= 0) {
reply.read(dstPtr, result);
}
跟进到BnCrypto::onTransact的case DECRYPT分支下,在该分支中,将封装在data Parcel中的数据重新取出,并做一些简单的check,然后交由serice组件的decrypt()函数最终执行。
} else if (totalSize > sharedBuffer->size()) {
result = -EINVAL;
} else if ((size_t)offset > sharedBuffer->size() - totalSize) {
result = -EINVAL;
} else {
result = decrypt(
secure,
key,
iv,
mode,
sharedBuffer, offset,
subSamples, numSubSamples,
secure ? secureBufferId : dstPtr,
&errorDetailMsg);
}
在以上两个check中,sharedBuffer->size()通过调用IMemory::size()返回结果,由前面分析可知,size()中调用了服务端的getMemory,如果service组件可伪造,则该值是可以任意指定的,因而以上两条check可被绕过。
继续跟进,上面的decrypt函数实际为ICrypto函数所声明的纯虚函数,在BnCrypto中没有实现,而是在BnCrypto的子类Crypto中实现的。Crypto类的实现位于 frameworks/av/media/libmediaplayerservice/Crypto.cpp&Crypto.h中,Crypto::decrypt()函数通过调用sharedBuffer->pointer()函数获取匿名
共享内存,并将其与传入的offset参数相加,作为srcPtr传入mPlugin->decrypt函数中,如下所示:
const void *srcPtr = static_cast<uint8_t *>(sharedBuffer->pointer()) + offset;
return mPlugin->decrypt(
secure, key, iv, mode, srcPtr, subSamples, numSubSamples, dstPtr,
errorDetailMsg);
mPlugin是Crypto结构体类型中CryptoPlugin类型的成员变量。CryptoPlugin的实现位于/frameworks/native/include/media/hardware/CryptoAPI.h中,它有两个子类,分别为android::MockCryptoPlugin 与 clearkeydrm::CryptoPlugin,这两者源码在frameworksavdrmmediadrmplugins目录下。
通过查看两个plugin的decrypt代码,我们发现mock plugin的decrypt函数只打了一条LOG,没有做其他事情,而clearkey plugin的decrypt函数比较有内容:
ssize_t CryptoPlugin::decrypt(bool secure, const KeyId keyId, const Iv iv,
Mode mode, const void* srcPtr,
const SubSample* subSamples, size_t numSubSamples,
void* dstPtr, AString* errorDetailMsg) {
if (secure) {
errorDetailMsg->setTo("Secure decryption is not supported with "
"ClearKey.");
return android::ERROR_DRM_CANNOT_HANDLE;
}
if (mode == kMode_Unencrypted) {
size_t offset = 0;
for (size_t i = 0; i < numSubSamples; ++i) {
const SubSample& subSample = subSamples[i];
if (subSample.mNumBytesOfEncryptedData != 0) {
errorDetailMsg->setTo(
"Encrypted subsamples found in allegedly unencrypted "
"data.");
return android::ERROR_DRM_DECRYPT;
}
if (subSample.mNumBytesOfClearData != 0) {
memcpy(reinterpret_cast<uint8_t*>(dstPtr) + offset,
reinterpret_cast<const uint8_t*>(srcPtr) + offset,
subSample.mNumBytesOfClearData);
offset += subSample.mNumBytesOfClearData;
}
}
return static_cast<ssize_t>(offset);
}else if( mode == kMode_AES_CTR ){
....
}
当secure为假、mode为kMode_Unencrypted时,程序会执行一个memcpy,从共享内存srcPtr中拷贝mNumBytesOfClearData字节的数据至dstPtr,而当服务端被伪造时,srcPtr可以设置成大于共享内存的地址,会造成越界读出共享内存之外的内容,并将内容通过dstPtr会返回给mediaService的Client端。
弄明白上述过程之后,PoC的编写流程即非常清晰了,共需要做两件事情:
1. 伪造一个实现IMemory接口的service,用以在执行IMemory->pointer()、IMemory->size()时返回指定的值。
2. 构造调用decrypt函数所需要的各参数,以便最终执行到clearkeydrm::CryptoPlugin::decrypt函数中的memcpy函数。
越界读PoC如下:https://github.com/b0b0505/CVE-2016-0846-PoC/blob/master/mypoc.cpp
在我的PoC中,采用的是如上文中所提到的伪造MemoryBase服务的方法,并重写getMemory函数,参考龚神的地方比较多,Orz膜拜龚神改虚表hook
写PoC的方法。
运行效果如下:
0x05 参考引用
2. https://github.com/secmob/CVE-2016-0846
3. https://bugs.chromium.org/p/project-zero/issues/detail?id=706
4. https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=226085
5. 《Android 系统源代码情景分析》 Binder通信以及匿名共享内存章节
还没有评论,来说两句吧...