[原创]CVE-2013-3660漏洞分析
人生第一次发技术贴,比较忐忑
这个漏洞出来一段时间了,一直都很感兴趣,最近才有点时间,就研究了一下,成果给大家分享一下。里面有不对的地方和比较2的调试方法,高手轻拍
文章稍微有点长,大家先看pdf,有问题一起讨论
--------------------------------------------------------------------------------
CVE-2013-3660 分析
by isng
简介
这个漏洞是 Tavis Ormandy 大神发现,可以本地权限提升,漏洞触发到利用都比较神奇, 就研究了一番,分析中可能出现不对的地方。
漏洞发生在 win32k.sys,path 子系统。主要触发在将贝塞尔曲线直线化的过程中。关于 path 子系统和贝塞尔曲线,有一些介绍:
http://msdn.microsoft.com/en-us/library/windows/desktop/dd162779(v=vs.85).aspx http://zh.wikipedia.org/wiki/%E8%B2%9D%E8%8C%B2%E6%9B%B2%E7%B7%9A
漏洞原理分析
关于 path 子系统的一些说明
关键数据结构:

图 1
Path 子系统主要实现了上面的三个结构(确切的说是类)。
PATHRECORD 结构是 path 子系统主要操作的结构,对其直线化操作就是对 PATHRECORD 操作。PATHRECORD 结构构成了一个双向链表,pprnext,pprprev 分别为前后项指针,flags 为 类型比如像 PD_BEZIERS。Count 为点的数量。POINTFIX 记录了各种坐标点。
PATHALLOC 是分配 PATHRECORD 的容器。当需要新建一个 PATHRECORD 时候,首先从 ppachain->pprfreestart 指向 的 地址 开 始 , 判断该 PATHALLOC 是否 还有空 间 分配 一个 PATHRECORD,若有则以 pprfreestart 指向的地址分配新 PATHRECORD,freestart 指针向下移 动;若空间不够,则分配一个新的 PATHALLOC,链入 ppachain 指向链表的链表头,再从新建 的 PATHALLOC 里的 pprfreestart 开始分配内存。由此可见,系统分配 PATHRECORD,实际上 是通过 PATHALLOC 结构实现的。ppanext 后向指针,pprfreestart 指向当前 PATHALLOC 内的空 闲空间。siztPathAlloc 是 PATHALLOC 的大小。当然 PATHALLOC 还有一些非常关键的静态数据 成员,比如 freelist,cFree 等,后面会再说。
PATH 结构主要被用在 EPATHOBJ 类中,这个类实现了一些操作 path 的函数,漏洞触发 的关键函数 pprFlattenRec 就在这里实现的。ppachain 指向了 PATHALLOC,pprfirst 指向第一 个 PATHRECORD,pprlast 指向最后一个。
freepathalloc 和 newpathalloc 函数
Freepathalloc 和 newpathalloc 函数是释放和分配 PATHALLOC 结构的函数。在 PATHALLOC 类中,有一个静态成员 freelist。这个 freelist 指向 PATHALLOC 的链表,链表最多有 4 个结点。 释放 PATHALLOC 时,先判断 freelist 里的结点是否大于 4,大于直接就用 ExFreePool 释 放掉;小于就链入到 freelist 里,然后 cFree 加 1(cFree 记录 freelist 有多少结点)。 新建 PATHALLOC 时,先从 freelist 里取结点,链入到 ppachain 上。若 freelist 为空,才通 过调用 PALLOCMEM()->ExAllocatePoolWithTag 实现结点分配。
有问题的地方
出问题的地方在 pprFlattenRec 函数的执行过程中,执行步骤大致为:
1. 参数为当前的 ppr
2. 创建一个新的 PATHRECORD *pprNew
3. pprNew->pprprev = ppr->pprprev;
pprNew->pprprev->pprnext = pprNew;
4. Flatten 操作???
……
if (newpathrec(&pprNewNew,&maxadd,MAXLONG) != TRUE) return((PPATHREC) NULL);
……
5. pprNew->pprnext = ppr->pprnext;
pprNew->pprnext->pprprev = pprNew;
实质就是把要 Flatten 的结点摘除链表,把新的结点插入原来的位置。第三个步骤是修改新结点前向指针,第五个步骤是修改新结点的后向指针。但是有一个问题,如果第四步, newpathrec 返回的不是 TRUE,即新建 PATHRECORD 不成功,就会直接返回。第五步 pprNew 的后向指针就没有被赋值了。如下图的样子。

图 2
如果函数直接从第四步返回,这个 pprnext 值会有两种情况。一种是 pprnext=0,另一种 就不一定了。pprnext 为 0,是因为创建一个新的 PATHRECORD *pprNew 时候,会调用 newpathrec->newpathalloc->PALLOCMEM , PALLOCMEM 分配内存后,会将这段内存的数据全 部初始化为 0,这样就不会产生问题。刚才说的另一种不一定情况是怎么回事呢?path 子系 统的内存分配依赖于 PATHALLOC 结构。这种情况有两种可能:
1. Newpathrec 判断,如果 PATHALLOC 结构的 freestart 指向的地址还有空闲内存就不调用 newpathalloc,而这个 PATHALLOC 分配的时候,可能是从 freelist 里直接摘下来的,这样就没 有 PALLOCMEM 置 0 的操作,这个 pprnext 可能就是上次内存残留的值。
2. Newpathrec 判断,如果 PATHALLOC 结构的 freestart 指向的地址空闲内存不足,就调用 newpathalloc,而此时 freelist 还有 PATHALLOC 结点,就直接从 freelist 摘下该结点,链入到 ppachain 中,再从里面拿一段空间给 pprNew。这种情况下,pprNew->pprnext 的值也是上次 内存中残留的数值。
这两种情况其实是最初的猜想,以 Tavis Ormandy 大神的漏洞触发方式,调试时候发现, 是第一种情况。
这样问题就比较显然了,这个时候,函数从 pprFlattenRec 返回到 bFlatten 函数,然后返 回系统调用。利用代码里再次调用 Flatten,会在 bFlatten 函数中,沿着 pathrecord 的链作一 些判断,当走到上次没赋值的 pprnext 时候,这个地址可能非法,就出错了。
还有一个关键性问题忘说了,就是会大量调用 CreateRoundRectRgn 函数,消耗内存池, 让 newpathalloc 分配内存失败,为什么用这个函数比较合适,没有再深入研究了。
调试过程
调试的 POC 是把网上下载的利用代码精简以后的。调试环境是 XP sp3
主要调试关键的循环,就是 BeginPath 开始的循环,经过几次循环后再下断点。 首先在 win32k!NtGdiBeginPath 会调用到 freepathalloc 函数,看一下这个过程。

图 3
因为已经调试过了,就直接在 freepathalloc 地方下断了。win32k!NtGdiBeginPath 会将上 一次的 pathalloc 回收,直接链入到 freelist 里。图上面内存 e117e214,e117e754 都是 PATHRECORD 结点。3e0000 是我们控制的 POINTS 地址。
然后看一下 PolyDraw 这个函数有哪些关键的动作。

图 4
PolyDraw 通过 GrePloyDraw 实现,看函数调用栈就可以大致有些发现,addpoints 加入 点的信息。先创建 PATHRECORD,然后向里面写入数据。当然 PATHRECORD 的创建依赖于 PATHALLOC,这里面直接从 freelist 里摘下链表,然后 cFree 减。可以看到,分配的 PATHALLOC
结构里还残留了上次的。调到这个时候,被同学关电源了,悲剧。。。数据可能对不上,流程 上还可以继续,不影响理解。
创建之后,进入下面这个 bXformRound 函数,进行数据复制操作。

图 5
第一个参数是拷贝源,可以看到里面是 3e000,第二个参数是拷贝目的地址,这里面是 上一次的数据 3e0000。这两个值差了一个移位操作。

果然,这里面有一个 shl esi,4。这就可以理解,为什么利用代码里会有 PathRecord 地址右 移操作。如下图

图 6
调试时候,这个 PathRecord 为 0x3e0000。
接着调用的 EndPath 感觉没做什么关键性的操作。
下面的 Flatten 能否触发漏洞,就有点概率性问题了,所以循环这么多次有这个因素吧。 这个地方调试的时候要循环很多次,只好用条件断点了。断点信息如下:

图 7
bf8b8b3e 0001 (0001) win32k!EPATHOBJ::newpathrec+0x67 ".if(poi(@eax)=0x3e0000){dd
eax;gc} .else{gc;}"

bf882074 0001 (0001) win32k!EPATHOBJ::pprFlattenRec+0x118 ".if(@eax=0){} .else{gc;}"

图 8
在 bf8b8b3e 下断点就是看看新建的 pathrecord 里面的值,可以看到现在这里面都是 0x3e0000。 然后 bf882074 地址处创建 pathrecord 失败,函数要返回。

图 9
可以看到,函数返回后,pathrecord 链里,已经有刚才新建的 pathreocrd 了。后向指针没有 被赋值,还是 0x3e0000,这次会返回到用户态。下次再调用 Flatten 函数,就会产生问题了。

图 10

图 11
漏洞利用分析
关于这个漏洞的利用,感觉 Tavis 大牛的方法巧妙。以他的这种思路利用这个漏洞,有 两个难点:
1. 如何转化成任意地址写
2. 其实后面可以看到,写入的 DWORD 是一个不可控的值,这个怎么利用
下面具体分析这个漏洞的利用方法
如何转化成任意地址写
先看一下 exploit 代码里相关部分

图 12
这里我们所控制的 PathRecord->next 指针指向了自身,flags 被赋值为 0。Tavis 的注 释也说的比较明白,flags 如果是 PD_BEZIERS(0x10),就会进入 pprFlattenRec()函数。为 了只是在 bFlatten()中无限循环,等待我们下一步的操作,所以这里赋值为 0。这个无限 循环,利用了 bFlatten()中的一段代码。

图 13
这里面 eax 是 path 结点的首地址。[eax+8]代表 flags,test byte ptr[eax+8],10h 就 是判断这个结点的 flags 是否含有 PD_BEZIERS。若有则进入 pprFlattenRec()函数,这里 面现在还不希望进入该函数,所以 flags 为 0。mov eax,[eax]取下一个结点,继续循 环。对应的源码如下图。

图 14
现在只是在一个地方循环,如何得到利用呢,看到 exp 中有这么一段代码。

图 15
这个函数是一个线程的执行函数,红色框中的函数比较关键。将 PathRecord-〉 next 的指针修改为 ExploitRecord 的地址。这个线程也就是使 PathRecord 的下一个结点 为 ExploitRecord(因为现在 PathRecord 地址为一个用户态地址,我们控制的)。那 ExploitRecord 这个结点里的数据是什么?

图 16
这里的 next 指向的地址为什么是这个,后面再说。prev 指向 HalDispatchTable[1]的 地址,有过内核漏洞利用经验的同学在这步应该能猜到,这个 prev 指向的地址就是要 写入的任意地址。这个 count 的取值没有作研究。
图 15 中的线程等待一定时间,就会执行,修改在 bFlatten 函数中无限循环的
PathRecord 的后向指针。该线程执行完毕,又会回到 bFlatten 函数中。此时就可以解析 到 ExploitRecord,由于 ExploitRecord 的 flags 含有 PD_BEZIERS,这样就会进入 pprFlattenRec()函数的执行流程,因此可以利用里面的对链表的一处操作,达到任意地 址写。
下面分析,pprFlattenRec()函数,如何导致任意地址写。

图 17
图 17 为 pprFlattenRec(PATHRECORD *ppr) 函数的一段代码。红色框内的语句是漏洞 利用的关键语句。pprNew 是 newpathrec 新创建的 pathrecord 结点。调用 newpathrec 后,有一句 pprNew->pprprev=ppr->pprprev。这个时候,ppr 为 ExploitRecord 的指针 (ExploitRecord 通过参数传入),这一句相当于 pprNew-> pprprev= &HalDispatchTable[1]。 又因为 pprnext 在 pathrecord 结构中偏移为 0。这样,红色框内的代码,就相当于修改 了 HalDispatchTable 表第一个函数的地址。是不是很像堆溢出的利用过程。本以为已经 可以利用的差不多了,但是这里出现一个问题。。。
写入的值貌似是个不可控值,pprNew 是 newpathrec 分配的(newpathrec 的实现前 面说明过)。这个就是最早提出的第二个问题。
怎么利用这个貌似不可控的值
图 16 中有一句代码没有解释,ExploitRecord.next = (PPATHRECORD) *DispatchRedirect;这个就是用来解决该问题的。

图 18
图 18 就是 pprFlattenRec()函数,在返回前,修改 pprNew 后向指针的操作。注意红 色框内的操作 pprNew->pprnext = ppr->pprnext; 这条语句,会向 pprNew 指向的地址处 写入一个 dword 数据,即*DispatchRedirect。调试发现,这个值为 40 ff 65 40 ,所代表 的汇编码正是 inc eax jmp [ebp+0x40] 。

图 19
这样,就相当于向这个不可控的地址写入了一个跳板指令。当我们通过调用
NtQueryIntervalProfile 触发 shellcode 时,会进入到不可控地址 pprNew 所指向的地址,此 时这个地址有指令 inc eax jmp [ebp+0x40]。Inc eax 不影响,jmp [ebp+0x40]会最终跳入到 提权的 shellcode 里。为什么 jmp [ebp+0x40]会跳入到 shellcode 中?
EXP 代码中有这么一句 Interval = (PULONG) ShellCode; Interval 会被当作 NtQueryIntervalProfile 的第二个参数,又因为
nt!NtQueryIntervalProfile->nt!KeQueryIntervalProfile-> call [nt!HalDispatchTable+0x4]。因此执 行到跳板地址的时候,ShellCode 函数的地址总是在栈上,而且只要这几个函数的参数不 变,这个偏移也都不会改变,这里是[ebp+0x40]。
利用过程中的其他细节
上面图 19 中那么多指令,而且还出现 inc eax 这种,这些看似很多余,有什么用意?由 于 pathrecord 链表已经被我们控制了,要达到利用效果,重要的是在向 HalDispatchTable 写 完数据后,可以从 bFlatten 安全返回。
上面解释了,为了利用成功,ExploitRecord.next= (PPATHRECORD) *DispatchRedirect。在 pprFlattenRec 返回后,会取到 ExploitRecord.next 作为下一个 pathrecord,若 ExploitRecord.next 地址未分配,肯定是各种异常。因此需要以*DispatchRedirect 为地址,分 配内存,为了用户态能分配成功,这个地址必须比较合适才行,因此就用到了 inc eax,和 后面的 jmp [ebp+0x40]组成了地址 4065ff40。当然为了防止在 4065f000 上内存分配失败, Tavis 又作了其他备选方案,就出现了后面的 inc ecx, inc edx, inc ebx, inc esi…..分别为 41, 42,43。。。。中间跳过了 inc esp ,inc ebp 这个原因比较显然,就不说了。

图 20
图 20 可以看到,依据*DispatchRedirect 分配内存,如果无法分配,就取到下一个 DWORD。直到成功分配。分配后还作了下面的操作。

图 21
结合图 16 中的 ExploitRecord.next= (PPATHRECORD) *DispatchRedirect。也就是使
ExploitRecord.next=ExploitRecordExit。ExploitRecordExit 的 next 域为 0,这样就可以在
bFlatten 函数中跳出循环,然后安全返回。可见这个*DispatchRedirect,将一个看似不可控 的地址转化为一个跳板,又可以使运行不出错(大致就这个意思,表达不明白了。。。)。
总想把里面的东西都说清楚,就写的有点乱了,其实调试一下就明白很多了。。。
漏洞利用的调试
这一部分只讲利用的过程,所以下了一个条件断点,bp
win32k!EPATHOBJ::pprFlattenRec+0x118 ".if(@eax=0){} .else{gc;}" 就是在 pprFlattenRec 函数 中,第二个 newpathrec 返回后,判断返回值有概率性的,有可能漏洞没有触发。。等于 0 就断下来。这个是
断下来后,返回到 bFlatten 函数里,看一下此时 pathrecord 链里面的东西。

图 22
地址 0x970000,就是利用代码里 PathRecord 地址,可以看到 next 指针指向了自身。说明 触发成功,接下来系统调用返回。第二次调用 Flatten,又进入到 bFlatten 函数。这时候就 在里面无限循环了,就不截图了,执行几次后,修改 PathRecord 的线程得到执行。我在调 试机上,用 OD 加载了漏洞利用程序(这个方法比较 2。。。。。。),这时看到 OD 断在了该线程 的执行函数里。

图 23

图 24
417020 就是 ExploitRecord 的地址,执行 InterlockedExchange 后,PathRecord 的 next 就被改 成 ExploitRecord 了。

图 25
这个时候要进入 pprFlattenRec 了。8054683c 就是要向该地址写入数据。

图 26
ExploitRecord->next 为 4065ff40,指向 ExploitRecordExit 结点,EXP 代码里事先已经在 这个地址上分配了内存。并且注意 40 ff 65 40 为 inc eax jmp [ebp+0x40]。

图 27
ExploitRecordExit 的 next 指针为 0,这样就可以在写完数据后,从 bFlatten()函数里安 全返回了。
进入 pprFlattenRec()函数里,在第一次调用 newpathrec 之后,观察一下 pprNew 的
值。

图 28
e29f9014 就是新创建的 pprNew 结点。接下来会对 pprNew 的前向结点赋值为 ppr->pprev。pprNew 的 flags,count 初始化,就不截图了。 下面就是关键的任意地址写部分了。

图 29
Esi 是 pprNew 地址,esi+4 是 pprNew->pprev(8054683c)。前面修改前向指针时,把 ExploitRecord 的 pprev 写入了这个地方,所以是 8054683c。执行第一个红色框,即 pprNew->pprprev->pprnext = pprNew
现在,在 HalDispatchTable[1]的地方如愿写入了一个值(pprNew 的地址),虽然现在这个 值看似不可控制。继续调试,一直到一个地方。。。

图 30

图 31
图 30 的这部分汇编指令,实现了 pprNew->pprnext = ppr->pprnext 。这个时候,地址 e29f9014 写入了跳板指令。这个时候基本任务就完成了。然后返回到 bFlatten 里,再取下 一个结点 ExploitRecordExit,由于这个结点的 flags 为 0,就不进入 pprFlattenRec()函数,下 一轮循环,它的 next 为 0,就继续返回了,然后返回到用户态。
最后调用 NtQueryIntervalProfile 触发漏洞,在 nt!KeQueryIntervalProfile+0x31 处下断
点。

图 32
这样可以看到通过跳板指令,直接跳入了提权 shellcode 的地方,401060 就是
shellcode 的起始地址,后面就不分析了,就是普通的替换 token 的操作了。最后就提权 了。
参考资料:
http://blog.cmpxchg8b.com/2013/05/introduction-to-windows-kernel-security.html http://www.exploit-db.com/exploits/25611/
王宇 epath_cn.pdf(就是台湾黑客年会的一个 ppt)
