欢迎来到 嗅灵易学

零基础也能上手的脚本技术课,一对一答疑带你入门

[原创]CVE-2013-3660漏洞分析

[原创]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)

上传的附件 CVE-2013-3660漏洞分析.pdf
poc_exp.rar
1.jpg
2.jpg
3.jpg
4.jpg
5.jpg
61.jpg
62.jpg
7.jpg
81.jpg
82.jpg
9.jpg
10.jpg
11.jpg
12.jpg
13.jpg
14.jpg
15.jpg
16.jpg
17.jpg
18.jpg
19.jpg
20.jpg
21.jpg
22.jpg
23.jpg
24.jpg
25.jpg
26.jpg
27.jpg
28.jpg
29.jpg
31.jpg
32.jpg
30.jpg

注意:上传附件及图片大小不得大于30M。

⚠️ 版权声明:
本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途。
未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。

0 0 0 举报
复制成功