这几天在twitter上无意间发现了一个讲类型混淆的paper,遂决定分析一下Windows内核中类型混淆的漏洞模式,本文也是基于两篇paper进行分析学习,首先介绍C++中类型混淆的原理,然后介绍一个Direct X模块的漏洞 。读完本文你会发现实际上后面这个内核洞和前面介绍的C++类型混淆关系不大,是的,我也这么觉得。这么做的原因是前面这部分适合拿来学习原理,原理介绍清楚了之后,后面就靠积累和发散了,实际上你会发现还是有很多相似点的,参考文章链接在文末
C++强制类型转换有很多种,这里简单介绍一下static_cast
和dynamic_cast
,在C语言中强制类型转换模型如下
// C double scores = 95.5; int n = (int)scores;
C++中强制类型转换使用方法如下,单从文字上看,static_cast
和dynamic_cast
区别主要是动态和静态,这使我联想到了动态链接和静态链接,虽然没什么关系
class B { public: int m_iNum; virtual void foo(); }; class D:public B { public: char* m_szName[100]; }; void func(B* pb) { D* pd1 = static_cast<D*>(pb); D* pd2 = dynamic_cast<D*>(pb); }
从汇编层面看的话,Debug版本静态转换并没有做过多的事情,只是单纯的保存了一下指针,只在编译时检查数据类型,没有在运行时进行检查,不过优点就是速度快,效率高
然而动态转换会运行时对指针进行检查,检查是否为空,检查类型是否正确等,但这也降低了效率,而且仅限于多态类,类似于CFG
之类的防护机制
以静态转换为例,下图中子类可以转换为父类的指针,但是父类不能转换为子类的指针,这样做会导致很多安全问题,下面有描述。
其危害如下图所示,当强制转换父类为子类时,调用子类的虚函数或者设置子类的一些新添加的字段就会产生访问错误,这也就是类型混淆的基本原理,然而在实际场景中这种低级错误肯定是很难遇到的,所以就需要发散一些新的思路。
关于利用理解起来也比较容易,两个不同类型的指针指向同一块内存,字段类型的不同会导致一些利用的机会,如下例所示
一个很简单的demo可以把上述过程描述的很明白,其结构如下图所示
在Linux下编译这部分代码可以弹出一个计算器,可以自己调试一下看看
// g++ test.cc -o test // test.cc #include <iostream> #include <cstdlib> using namespace std; class Base { }; class Exec : public Base { public: virtual void hack(const char* str) { system(str); } }; class Greeter : public Base { public: virtual void sayHi(const char* str) { cout << str << endl; } }; int main(int argc, char* argv[]) { Base* b1, * b2; Greeter* g; b1 = new Greeter(); b2 = new Exec(); cout << hex << b1 << endl; cout << hex << b2 << endl; g = static_cast<Greeter*>(b1); g->sayHi("hello world"); g = static_cast<Greeter*>(b2); g->sayHi("/usr/bin/xcalc"); delete b1; delete b2; return 0; }
上面的内容主要讲述的是对于C++类型转换可能存在的问题,然而我只关心在Windows内核中这种漏洞模式是否常见,在Windows内核中关于类型混淆的洞公开的资料比较少,在Adobe,浏览器中出现的比较多。Windows内核主要还是C编写的,所以上面出现的问题实际上很难遇到,下面这个洞实际上和上面C++这个模型没啥关系,不过拿来发散学习一下思路也是极好的,这个洞是在2018年被湛卢实验室的师傅们fuzz到的,这里不过多的讨论fuzz相关的内容,主要是理解一下这个洞的原理。这个洞存在于 dxgkrnl.sys
,首先看一下漏洞对象的创建过程。
创建对象逻辑如下,其中有一个 isCrossAdapter
跨设备标志,如果设置了此标志就会申请 0xE8
的内存,并且会设置一个0x20的标志用于后续的检查,如果没有设置跨设备则直接申请 0xC0
大小的内存。这个漏洞也就是手动构造了0x20的标志,导致后续逻辑认为申请了 0xE8
大小的内存,然而实际只申请了0xC0
的内存
漏洞点 DXGDEVICE::CreateAllocation
函数部分逻辑如下所示,首先检查了0x20的标志,也就是上面创建0xE8
大小对象中设置的标志,如果检查通过则进行下面的步骤,其中v175~v177
均是用户可控的数据。也就是说,漏洞函数判断对象大小的依据是这个0x20的标志,然而触发者如果手动构造了这个标志,然后传入0xC0
大小的对象,这样类型混淆就会导致越界写的问题,从而导致了漏洞
Patch是在 DXGSHAREDRESOURCE::CreateSharedResource
创建完对象之后添加检查,也就是在上层DXGDEVICE::OpenResourceObject
函数中对这个0x20标志的地方进行检查
前面提到上面这个漏洞是fuzz出来的,附带的还有几个漏洞,感兴趣的朋友可以看看参考链接里面ZDI的分析文章,如果肉眼审计的话肯定是很难关注到这个点的,一般这种fuzz出来都是越界读写导致的crash,需要开启特殊池,对于没有crash的类型感觉还是很难fuzz出来的。
对于审计而言我认为主要关注点还是应该在创建对象的地方,上半部分C++类型混淆的paper中也讲到了他们是如何对类型混淆进行检查的,他们强制对所有对象运行时进行检查,分析这个对象申请内存的地方,跟踪对象分配类型。也写了一个工具出来,感兴趣的朋友可以用用。