2016-04-20 10:06:02 来源:360安全播报 作者:bobb 阅读:384次
分享到:
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,这两者源码在frameworks/av/drm/mediadrm/plugins目录下。
通过查看两个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 参考引用
1. http://androidxref.com/
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通信以及匿名共享内存章节
本文由 360安全播报 原创发布,如需转载请注明来源及本文地址。本文地址:http://bobao.360.cn/learning/detail/2847.html