Pwn2Own王宇:针对黑客比赛当中安全漏洞分析

王宇:大家好,这是我们Pwn2Own的一个分享,可能今天的议题和ZDI的同事们会有很多重复的地方,我可能会更多的描述一些漏洞细节的内容。

其实我也是在比赛结束之后,第一时间去寻找漏洞的内容,寻找出了什么问题,今天算是对于我之前分析的一个总结。今年的Pwn2Own内核提权一共是五个,我排了一下顺序,前两个是我打算分析尹亮同学的一个内核的提权,一个信息泄漏,还有两个来自360的提权。

第一个漏洞,出问题的函数我命名成不一致型的漏洞。我们先看看这个补丁的细节,左边是补丁之前的内容,第一点是补丁删除类型的,这个不是太多见,因为一般我们见到的补丁都是添加一部分的逻辑去修正之前的那些问题,而纯粹的删除去面对问题这样的几率不是很多。

我们发现在有问题的代码里面,某一个结构,RCX+40偏移的位置做了一个判断,基于这个判断去做了一些Return值的一些选择,可以Return0或者是1。给我们一定是两个问题,第一个问题是RCX只是一个结构,RCX+40是一个什么问题?

这个漏洞本身,为什么删除这个代码这个漏洞就不存在了?随着往后的研究,我们发现根据这个漏洞相关的结构叫做PFF,PFF是自己比较高层的一个结构。我们知道这个结构叫PFF之后,下一步一定是去找到它的结构的定义。

上哪里去找这些结构的定义呢?最左边的是来自于比较靠谱的一份结构,这个结构偏移量是基于32位的,所以给出的注释都是基于32位偏移。右边是不太合法的,是来自于微软泄漏的一个样本,在座的各位可能是微软的前雇员,所以我们很多就不展开怎么去弄这些东西。

但是实际上这个代码的参考价值不是太大,因为这个代码的时间非常久远。但是我们可以看到这么几样东西,我们可以看到有一样东西是CR Funt,还有我用红色和蓝色标出来的东西,蓝色对应的就是OX+40便宜的地址,红色的是另外三个偏移。

继续看这个结构的生命周期,因为我们看到一个数据结构之后一定是有它的生命周期的,我们要知道是什么时候重建,什么时候被销毁,以及什么时候再利用。下一步我一定要对OX+40这个偏移下一个断点,看一下什么情况下会被引用。

反过来我们能看到,首先可以看出这个地方一定是一个相关的东西。能看得出来,这两个都是跟这个Funt相关,所以能看出刚才那张图里面,+40偏移这个C一定是代表的Funt,所以也能印证我这个结构偏移角应该是对的。也就是说,这个是操作PFF结构的这么一样东西。

再往后看,下一步我一定是关心这个结构什么时候会销毁,这是关系到一个结构完整生命周期的问题。我们找到了一个Hyper,它会去做一些判断,对这个结构做销毁。怎么销毁呢?我销毁不能说是莫名其妙的就销毁掉了,一定是做了一些判断。

我可以看到它做了哪些判断,它判断了加某一个结构,这个结构是PFF,他判断了+38偏移,判断了+3C偏移,判断了+A0偏移,下面就可以被销毁。反过来再回头看第一页图上+40偏移,这个偏移就很尴尬。也就是说我定了一个规则,销毁的时候并没有按照这些规则,

销毁的时候判断的是38、3C和A0,另外一个地方判断的是40偏移,这两个地方就存在着不一致性的问题。也就是说我定了一套标准,所有人都应该遵守,而不应该是我判断了这几个,我就认为可以被应用,而另外是另外的东西,这是存在不一致性的东西。

我们再往后看刚才出问题的那个函数,如果他Return是True会执行这样一段代码。但是如果是我用灰色标出来的代码是会被跳过的。跳过的结果是,这个字体在一个全局的变量,在全局的结构中并没有被擦除,它的擦除动作是用下一列表覆盖掉,把自己抹掉。

也就是说如果这段代码跳过了这样一个东西的话,这个结构的指征会永远的存在Private全局的结构当中。再回过头来我们就能看到,最上面的是内核去引用了这样一个全局变量,这个全局变量的某一个偏移存了一个指征,结尾是3:50。我们去看的话,其实是一个被Free掉的内存。如果有人再去基于这个全局的结构引用的话,这就是一个标准的漏洞。

懂行的人下一个问题一定是问,这个内存在什么样的堆上,这个堆的大小是多少,这个Pool的Side是多少。懂行人看到这一页图一定是看到这两个位置,它的大小是0X140,这两个问题就决定了我在什么样的堆上,分配多大的内存去占位这个结构,然后再往后的问题是我选取什么样的结构,
基于这个结构用什么样的方法可以做到Read,用什么样的方法可以做到Write,把这样一个问题转化成一个内核的问题。所以我们可以看到,真正的利用过程,我后来选的对于内存的占用。

占用完之后,把某一个偏移量,就是已经被Free的内存,在系统有一个VQ的函数,这个函数是一个挺倒霉的家伙,它会用刚才被Free的Pool。这个时候他会对我提供数据的某一个地址做一个写操作,只要把某些敏感的地址填进去,系统就可以去掉。

既然有这么靠谱的,下一步一定是提权,没有任何问题。访问一个地址,立马就可以弹出来,就是这个原因,因为这个漏洞本身没有太多的不确定性。

反过来,这个问题只是一个恶意地址写的问题,懂行的人一定会问下一个问题,就是我这个地址写哪儿了,内核有内核的计划,我把什么样的地址写到里面,这就引出了下一个漏洞。

有一个内核的信息泄漏的问题,这个漏洞也是来自于电脑管家的尹亮同学,它的编号是20160175,出问题的函数是这个函数,NtGdiGetEmBUFI。这个历程是一样的,左边是有问题的函数,右边可以看出加了一个逻辑,老的代码是从某一个地方的RAS取出来一个值,把这个值填到20里面去。

因为是信息泄漏是我们这个漏洞的背景,所以一定RAS里面填的是一个内核地址。这个内核地址被写入一个用户的数据里面去,所以把一个内核地址泄漏出来了。现在是把一个内核地址取出来之后,又把RAS里面的值,8C偏移的那个值取出来,把这个值放进来。

多半来讲,我们对于这个内核这么多年的了解,各位的了解来讲,多半都是把一个原本的地址现在换成了一个用户态的指征,这个补丁其实修复的很简单。

我们可以看一下,这里是出问题的,用的是64位的。所以可以看到,这一条目移到24的时候,我们可以看一下参数,24是一个Buffer。Kernel会填进去PGFF,大小是0X140的一个Pool,他把这个Pool的基地值返回,这个Pool在刚才的第一个漏洞当中是非常重要的,

这个Pool就会发生Free,这个函数又会非常漂亮的把出问题的Pool值返回,就像神配合一样,把出问题的Pool的值返给用户态,用户态又知道哪里做这个Free,这两个合作可以说是天衣无缝的。这就是尹亮同学这一套提权利用的细节。

再往后可以看到补丁是怎么修复的。补丁还是从这个基地值取出了这个Pool的值,Pool+8C的时候,取出了一个0700B4的东西,这个东西是放弃了对应的Handle,这个时候把Handle回填,新版的补丁后的内容,把一个内核堆上结构对应的Handle回了回去,这个回是没问题的。

再反过来看这个源代码,大家应该有点耳闻,是一个民间自发写的一个微软SP版本源代码公开的东西。所以可以看到,它在定义这个PFF结构的时候,32位加偏移的时候显示On No,就是对应的这个结构的Handle。所以大家感兴趣,想对开源社区做一些贡献的话,可以把这个改成Handle之类的东西。

刚才讲到这个泄漏,实际上我觉得这个话题倒是,泄漏无处不在。我记得我在去年GeekPwn公开课的时候举过这个例子,这个函数我们可以看到那个地址泄漏出一个内核的指征,也就是说,它会把整个的内核指征直接泄漏到这个当中去。

我截图的这个时候是10月份的,11月的补丁是昨天的。我试了一下11月的补丁,这个问题依然是存在的。但是这一类的问题在当中会非常多,多到以至于这个东西没有任何的价值。非常多的这些从何而来呢?

我写过一个小工具,这个工具我相信在座的各位写用用的时候应该都会写过,可以到每一个内核的对象,他们的Type、Basic Address。最夸张的是堆加密的头,我们知道在Windows10等等一些系统上面,微软为了防止别人猜到他们堆头的信息,他们进行了加密,加密有一个总则,

这个泄漏甚至把这个堆头加密的总则泄漏出来了,我可以反推出来,也就是说内核已经泄漏了足够多的信息了,根本不在乎这一两个泄漏堆地址,泄漏函数的基地址等等。所以我觉得就是,如果微软想继续提高系统的安全性,这种机制至少是应该被限制的。不可以让用户这么轻易的得到内核的布局,内核的地址信息,对象的地址等等。

这个也是选取的一个图形的东西,是DXG模块的一个组件,在图形显示的时候,会调用到这个组件做一些渲染。这个漏洞其实是这五个漏洞里面最低级的一个,至少我们评估程序员犯错误的时候,这个犯错误的水平是最低级的。

我们可以看到有一个函数,它要做Momery Copy,我们要知道地址是哪里,源是哪里,最关键的问题是拷贝多大。源是我们应该控制的,我们提交多大这个东西至少是应该有限制的,因为我不可能提交1万字节你就给我拷贝1万字节,如果能够把1万字节分配出去也没有问题。

但是实际上他可以不做任何的检查,就给我往任何一个地方做拷贝。这个漏洞实际上不太高级,犯错误的程序员应该试着检讨一下自己。作为检查,我们可以看到这个新的补丁至少是对长度做了一些判断,在长度小于0X437的时候才会去做一些相关的拷贝操作。

我这里截了当时我做分析时候的一些截图,就是我可以控制,这个是一个非常巨大的指证,他会做一些相关的计算。比如说有恶意操作,有一些+334的操作,加完之后,我们可以看到这个Memory Copy的分别是一个内核的原地址,一个是内河的目的地址。

我们可以看到这个是基于做运算而做出来的,这个非常大,它计算出来的结果一定是导致一个堆。
如果大家好奇刚才的那段操作是怎么做出来的,刚才是如何到DA、DB、F22E,怎么到这个值的时候,可以看到它的计算其实是基于它的数据结构的。这些结构在Windows、WDK10,在这些位置都可以找到这些结构的基本定义。

所以我相信在座的各位如果去看皮特最新的演讲上,他们也有详细的介绍,所以我就不用多讲了。但是我引用了他们一页幻灯片,是取自他们的研究成果。这张幻灯片我一看就笑了,原因是因为我们遇到过同样的问题,这里抛出来的一个问题是说,他们做了很多实验,不太好去写这个东西。

原因是因为目标地址刚才是长度、源和目标,目标地址是一个Slist,这是一个单元,是一个单链表片出来的结构,每一个结构指向某一个地方。如果我越界的话,一定是越到下一个结构上去。那么我非常希望有人用我越界写的这个内容才可以,因为这个时候才能继续往下走。

我们发现一个问题,就是我很难找到第二个并发的人,很多情况下我们在调试的时候,我们发现有很多非常大的,很难有人跟你去做并发,他们遇到什么问题很难做并发。所以提出来的问题就是说,这真的是一个有人可以去做第二次POP的操作吗?

理论上如果不是一个并发的话,根本没有必要所谓的SList去做,这一点可以看出来Keenlab的同学想法是非常正确的。我做过一个事情,就是写一些脚本,跑一些图形密集的程序去抓,所以他们的当空接龙游戏,完成了这个并发,通过学习它的行为,我们就能知道我们应该如何写这个代码。

最后用了多线程的方法调用某些函数,去访问一个被你溢出的Pool。我相信在座的各位只要是去尝试写过这样的东西,反过来,其实这个漏洞的利用还有一些问题,就是假设我们在座的所有的各位是一个班级的,每个人都有一个学号。

如果溢出是按顺序做溢出的,我取学1号,那个相邻的人不一定是2号,2号可能在那个位置,所以这个时候整个内存的写入控制其实是需要做一些精准的调整的,这个就看写漏洞的人本事了,想把一个漏洞写得标准没有那么容易,在这个里面需要做一些优化。

总体来说刚才那个漏洞还是一个标准的内核堆的溢出。还有一个问题是来自360的,出问题的函数是在GreRestoreDC,我认为这是一个逻辑,这个逻辑的后果导致的。左边是补丁之前的,右边是补丁之后的,相对来讲不太那么好看,因为这个补丁之后的逻辑变得很复杂。

但是实际上一句话就能说明白,而且是一个非常低级的问题,但是这个低级的问题很难被发现,原因是和这个漏洞相关的一共是两个结构,第一个结构是一个外层结构,它的名字是DC,DC内部有一个结构,它的名字叫Service。

出问题的时候,我们在访问DC的Surface对象的时候,发现我们的Surface已经被Free掉了。反过来作为漏洞分析的人员,一定要紧盯Surface这个结构。我对Surface这个结构做了一些断点的监控,我们可以看到,这个结尾是BAC0的地址是一个Surface的。

它的+C偏移,64位的一个+C偏移的地方我们可以看到被增加的地方,还可以看到一个被减少的变量,就是有人加,有人减。再往后跟踪,我可以发现在这个时候做了加操作,再往后做了一个减操作。

最后一个关键的,我们发现又做了一个减操作。也就是说操作理论上一般都是成对的,有加有减,函数入口加,函数出口减。为什么在一个RestoreDC里面连续两次减?这是一个值得深挖的问题。

写Windows图形相关程序的人大家知道,这个概率非常大,我们可以看出,甚至它处于一个模块,也就是说Base模块是把最强的东西取出来放到里面,这个里面都是很基础的东西。

我们就能看到,同样的RestoreDC会调用一次减少,RestoreDC调用了国内的之后又会做一次减小,这是我们要找的原因,就是为什么会连续做两次。我们看到左边的代码,左边的代码我们可以发现做一个判断,如果怎么样,做一次解码操作。

紧接下来又是一个什么东西,做一个调用,这个变量的内部又会做一次减法操作。反过来我们就可以问自己一个问题,如果我有一个条件能同时激活这两条的话怎么办?是不是这个程序就会连续被减了两次?答案是肯定的。

不会让一个对象连续被减两次,所以这个补丁非常简单。反过来就可以看出,当时写程序的人其实脑子也没有想明白,他并没有想清楚这几条并发的逻辑分支应该是可被同时执行调用的?还是说每一个执行序列只能选中一条,所以这个可能是因为前期的复杂性导致的。

攻击代码其实就是尝试同时激活这两个分支,让这两个分支同时被调用,这个就是这个漏洞本身的问题。因为它同时被做了Restore两次减法,所以会提前结束。下一步一定是他的对象的字节是370个,它来自于Surface这个对象的分配。

这个时候作为内存占用,我们可以找相同堆上的大小,可以扩展到370个字节的对象做占用,BitMap可以做这样的应用。

最后一个漏洞也还比较有意思,是来自于这个函数的Use After Free。大家可能比较好奇,有的时候是以FFF,有的是以XXX,有的是ZZZ。XXX是表示这个函数会做一个内核的同步调用,

ZZZ表示这个函数会做一个异步调用,实际上他们是给程序员的,表示你看到这个函数,就知道它可能会发出一个请求,然后发到一个用户里面去。看到这个名字,多半都会有一个意识就是Call Back,就是说我这个函数可以发出一个Call Back。

Call Back之前和之后是要做一个很严格的检查,我不知道各位有没有概念,实际上我们可以这样想,如果在座的各位是高权限的,他也不能代表所有人,他也要别人帮他做一些事情,他会把控制权交给别人去完成。

反过来如果我把控制权交给别人完成,回来的时候,我是不是要对这个回来的结果做一个完整详细的测试呢?就是有的时候你并不能把东西交给别人回来不检查,这是不可以的。以这种方法,我们能够找到一批的漏洞。

比如说今天上午ADI说的那个问题,源自于模块之间的互相调用,导致在回来的时候出问题。Use After Free Call Back是回来再做检测,而检测不过关出了问题。再往后我们想,我不可能把所有的访问都做了,我意识到在外面做一些事情。所以这个时候互相的信任和调用是不可避免的,但是你们要相信这种信任。

这个例子就是一个标准的Use After Free Call Back,干了一件事情,这个时候这个Handle就已经没有意义了,是一个毫无意义的。但是内核拿到这个Handle在用,补丁是拿到这个Handle之后,对Handle做了有效性的验证,然后加了应用,最后才把这个东西传到内核里面去做二次调用,这是两边根源的差异。

我们可以看到有一个内核的函数,发出了一个调用,这个调用会通过这个接口反馈回来,这个就是一个交互的过程。关键的问题就在于出去之后再回来,我们可以看到最那边的数字,11512这是一个Handle,这个Handle我们通过查重表,查内核的一个表,查出来它对应的这个内核的对象是41930。

这个对象我们可以验证一步,因为访问这个对象的第一个域,它可以反向指向这个对象的Handle值。我们可以看出,这个对象是有效的,是指向某个地址,这个地址是这个对象的值。

我们看到紧接下来,这个15012这个地方,立马会变成一个被Free的内容,所以说这个对象已经被Free掉了,这种东西是一个非常传统的,很古老的一个方式。我们应该怎么做?

作为漏洞的利用,我们如果去对比这个漏洞和第一个漏洞,虽然大家都是Use After Free,但是第一个Use After Free大家争抢的是某一个地址,我们一定要把那个被Free的地址调用出来。这个地方是用聚敏,我们一定是抢这个聚敏值,我们看哪一个Object可以把刚才那个Handle找出来,这个就是利用上的一些差异,但是实际上理论上是一样的。

这样做的结果是,我们可以看到这个地方就在索引,也就是说,这个聚敏值已经被Close,又重新出来之后,这个地方还缓冲了这个聚敏,但是实际上这个聚敏已经对应不上了。我们可以看到,如果我们没有去做内存占队的话,那边表示这个地址也是一个非法的。

我这个时候用的是ACCER这种结构,结果是系统要帮我到每一个地址,这个时候就能达到这个结果。为什么这两个漏洞不需要信息泄漏就能够达到利用呢?因为利用的方法基本上一个漏洞就可以达到两种方法同时,就是读和写都能知道要改哪儿,改什么,都能同时去解决,一个漏洞可以解决所有的东西。

刚才漏洞讲完了,就是今年所有Pwn2Own的一些细节。他们还不是利用起来写的比较复杂一点的,我曾经挑战过CVE-2015-0057的漏洞,这是由以色列一个人发现的,他发了一个Blog,我当然看到这个漏洞,我发现我控制内存的时候,我对这个对象进行读写的时候,我并不能完全的操控这个内存。

也就是说,蓝色的部分是我可以控制到的,而红色的部分是我不能控制到的。其实用这种对象去做内存的读写的时候,我的能力总是受到限制,这个读写能力都是残缺的。怎么去解决这个问题?

我当时用了两个对象去交错的读写,能看到这两张图的差异,第一张图是我在状态一时刻的读写,第二张图是我在状态二时刻的读写。

通过这个对象的错位,我能完整的去写一个32字节的地址。所以到后来对象的布局变得比较夸张,但是最后的结果是我可以在64位上通过交叉去表现所有内存的写入。我本来有一个Demo,如果大家愿意看的话可以看今年的Black Hat上我的演讲,上面有一个演示。

最后提两个,一个是今年的,一个是去年的,分别是2546和0167,我们有两个Blog。每个出问题的地方都是xxx的Use After Free Call Back里面。我想说一下它的细节,这个漏洞,包括2546,我们发现它的作者都是来自于同一个人,我们对后台对这个人做过监控。

这是当时我们抓到的他两个样本,这两个样本在Web上是没有的,当时的演示Demo,我可以说一下大家的关注点。实际上在我们追踪这个对象的时候,用户可以去创建一个对象,但是我们去分析发现,Kernel也会去调用,他也会在Kernel创建一个对象。

这个时候就有几个问题出现了,我们可不可以把这个内核自己创建的对象的窗口去抓走?我们可不可以把内核创建对象的窗口对应的地址抓走?所以如果我们基于内核对象去偏离核对象,这个时候系统的检查往往是有的,这几个漏洞都是从这个角度去挖的。

最后是一些总结,我觉得今年我们看到都是来自于中国人的,还是很长志气的,确实这个能力,吴老师团队的能力,包括像360的能力是相当不错的,这一点是我们觉得欣慰的。

我觉得能力这个东西是可以从一个领域到另一个领域的,可以发现吴老师的团队做到内核,做到汽车,实际上他们的方法论是一样的,也就是说找到了比较好的调试的方法,你看所有的东西,其实道理是相通的。

最后一个是明年的Pwn2Own,可能要作为漏洞的利用的话要找新的突破口,我觉得在座的各位可以借鉴一下刚才几位演讲者的奇妙的想法,谢谢大家!

上一篇:Intel李海飞:从用户角度分析微软Office的攻击界面

下一篇:腾讯聂森+刘令:特拉斯网关的逆向揭秘