0x01 前言
- 微软提供一个叫 Cinepak 的视频解码器,通过调用 iccvid.dll 这个动态链接库文件可以使用这个解码器;微软自带的 Windows Media Player(视频音频软件)通过调用 iccvid.dll 解析有漏洞的 RIFF 音频文件格式时会触发 CVE-2010-2553 这个漏洞
- 该漏洞的成因是由于 iccvid.dll 中的 CVDecompress 这个函数在解析漏洞文件时没有对 cvid 格式编码条中的 Chunk 数量做限制,导致连续复制数据到堆空间,最终超出,导致堆溢出
0x02 调试环境
- 虚拟机:Windows XP sp3,版本:11.0.8211(提取码:woi9)
- 调试工具:Windbg(Windbg32,Windbg64)、OD、IDA(提取码:v0pt)、C32Asm(16进制分析文件工具,提取码:4sj8)
- 漏洞程序:Windows Media Player(提取码:07n9)
- 实验样本:能触发漏洞的 .avi 格式的 POC 文件(提取码:5ilg)
》每台机器调试时的地址可能会不一样
0x03 关于 cvid 的格式
- cvid 是一种压缩格式,嵌入在 .avi 格式之中,在使用它这个格式时需要对其进行解压缩
- 下图就是 cvid 的格式,由格式头(Frame Header)和 body 组成,body 里面就是顺序排列的编码条,每一个编码条都包含一个编码条头(Strip Header)和多个 Chunk ID + 对应数据的 Cvid Chunk 组成的结构,这些结构按顺序排列在编码条头之后
- 这个就是样本中对应的 cvid 格式
Flag:00(1个字节)
Cvid 数据长度:00 00 68(3个字节)
编码帧宽度:01 60
编码帧高度:01 20
编码条数量:00 10
编码条 ID:10 00
编码条数据大小:00 10
顶部 X 坐标:00 00
顶部 Y 坐标:00 00
底部 X 坐标:00 60
底部 Y 坐标:01 60
Chunk ID:20 00
Chunk 数据大小:00 00
Chunk ID:11 00
Chunk 数据大小:00 10
Chunk 数据:AAAAAAAAAAAA(12个字节,0x10 - 4 的结果)
Chunk ID:11 00
Chunk 数据大小:00 10
Chunk 数据:AAAAAAAAAAAA
Chunk ID:11 00
Chunk 数据大小:00 10
Chunk 数据:AAAAAAAAAAAA
0x04 根据异常定位漏洞函数
- 打开 Windows Media Player 软件,用 Windbg 附加进程
- 开启堆页保护之后运行,之后将 POC 拖入 Windows Media Player 内,成功捕获异常,出问题的动态链接库就是 iccvid.dll,该链接库在 system32 中;因为开启了堆页保护,所以可以断定 rep 循环复制命令复制了超量的数据导致了堆溢出
- 使用 !Heap 列出程序使用的堆空间列表,查看 edi 的值,发现在第一个堆中;为什么查看 edi 的值呢,rep 指令将 esi 指向的数据复制到 edi 的地址之中,因为造成了堆溢出,所以此时 edi 指向的地址一定在堆中
- 之后使用 !Heap -a 0090000 查询堆中的堆块信息,发现 edi 所指向的地址在这一个堆块当中,0x00156e40 这个堆块就是有问题的堆块,该堆块的下面还有一个 0x15ce48 的堆块,继续溢出的话会对这个堆块造成影响
- 将 system32 中的 iccvid.dll 载入到 IDA 中,找到 0x73b722cc 这个地址,F5 翻译成伪 C 代码
- 找到了问题函数 memcpy,这个就是 CVE-2010-2553 漏洞触发的根源
0x05 根据样本调试分析漏洞成因
- 与以往的漏洞不太一样,该漏洞所调用的 memcpy 函数并不是一次性的将数据复制到堆中,而是循环调用该函数复制数据到堆中,直到堆中数据溢出
- OD 载入程序可以选择先打开程序(也可以使用 Windbg 或 Immunity Debugger 调试,根据个人喜好),再附加进程,就像这样,你也可以找到源程序再拖入 OD 运行,一个道理
- 附加完进程之后,这时下 0x73b722cc 这个异常断点是断不下来的,因为此时程序还没有加载 iccvid.dll 这个动态链接库(0x73b722cc地址是动态链接库里面的)
- 这里可以采用等程序异常终止以后再下这个断点,就像下面这样,此时已经加载了 iccvid 动态链接库并且可以下断点
- 如图所示,已经在异常处断下
- 在断点断下后,需要找到函数的入口,从函数入口调试有利于理解运行流程
- 如何找到函数的入口,很简单,可以查看栈中返回值
- 查看栈,找到一个返回值,使用 enter 键跳到这个地址
- 假如有些调试过程中这个地址可能不是想要寻找的函数地址,这时可以继续沿栈向下查找返回值,多试几次就可以了(当然,在栈中的数据完好不被恶意覆盖时可以这么做)
- 可以发现就是这个函数调用了 memcpy 将数据复制到栈上的
- 下断点之后重新运行
- 之后 F7 进入这个函数,来看看函数对样本数据做了哪些操作:
- 首先第一步>>>先对比 Cvid 数据长度是否大于 0x20,之后会将 Cvid 数据长度的第一个字节和第二个字节分别放入寄存器 eax 的高位和地位,我们将此时的 eax 中的数据暂且称为 result1
- 然后将 result1 和 Cvid 数据长度的第三个字节做积或运算,我们将运算的结果称为result2
- 如果 Cvid数据长度小于 result2 或者 UlongSub 函数的返回值小于 0 的话就进入 if 语句
- ULongSub 函数的功能是将第一个参数的值减去第二个参数的值赋到第三个参数的地址中,如果第一个参数大于第二个参数就返回负数,反之则返回 0
- 第二步>>>判断编码条的数量是否大于 0
- v11 存放的是 POC 数据的地址,v13 的值是 POC 地址偏移 0x8 个字节,刚好是编码条数量的地址
- 第三步>>>比较未解压缩数据的长度是否小于 0x16
- 此时未解压数据的大小为 0x5E,由 UlongSub 函数计算未解压数据的大小
- 第四步>>>取出编码条 ID 的第二个字节和编码条数据大小的第一个字节分别放入寄存器的高低字节,我们暂且将这个值称为 res1
- 之后 res1 和编码条数据大小的第二个字节(0x10)做积或运算,运算完的值称为 res2
- 最后判断未解压数据的大小是否小于 res2
- 第五步>>>判断编码条 ID 的第一个字节是否等于 0x10 或者 0x11
- v11 指向 POC 的地址,v14 是 v11 的地址加 10(0xA) 后地址的值,用于读取 POC 样本中的数据
- v14 指向的 POC 地址为编码条 ID 的地址 + 0x1值,也就是编码条的第二个字节 0x10
好像确实有点复杂,多调试几次就可以了
- 最后一步>>>首先判断引用计数是否不为 0,由此可知 do - while 循环第一次并没有执行 memcpy 函数,其次判断 Cvid 数据长度的第三个字节是否不为 0(BTYE3 函数取目标的第三个字节),最后判断 Chunk ID 的第一个字节是否等于 0x11(十进制 17)
- 在满足上述所有条件后才会执行 memcpy 函数
- 逻辑图如下图所示,只列出了相对重要的步骤
- do - while 循环结尾如下图所示,v28 是引用计数,v32 控制着 memcpy 函数的第一个和第二个参数的值
- 下面就是 memcpy 这个函数,v32 每次递增 0x2000;参数一和参数二由 v7 和 v32 控制,参数三也就是复制数据的大小为固定的 0x2000
- 所以按理说执行条件满足后执行 3 次就会造成堆溢出(上面 Windbg 调试出堆的大小为 6000,因为堆中还要存放其他数据,所以 3 次足够了),由于初次进入循环时引用计数为 0,所以并没有执行 memcpy 函数,所以加上这一次,在 do - while 执行到第四次循环的时候才真正的溢出
注意堆虽然会溢出,但是并不代表一定触发异常
0x06 漏洞是否可以利用
- 反观 memcpy 函数,发现只有 v7 的值相对可以被控制,下面是函数的追根溯源
- v7 = a1,a1 是 CVDecompress 的第一个参数
- CVDecompress 的第一个参数由 a1 控制
- a1 由 Decompress 的第一个参数传入
- 第一个参数由 v5 控制
- v5 又是 DriverProc 函数的 dwDriverIdentifier 参数传入,貌似传入的是一个地址
- 调用关系就是 dwDriverIdentifier() -> Decompress() -> CVDecompress() -> memcpy()
- 那么 dwDriverIdentifier 是怎么传进来的呢,答案是由源程序传入,并不是 iccvid.dll 中的数据,也就是说程序在调用 iccvid.dll 链接库时才将 dwDriverIdentifier 传进来,故漏洞利用的难度是非常大的
- 再往上追根溯源的话就已经超出了 iccvid.dll 动态链接库的范围了,与本文主题不符,故不做过多阐述
- 参考资料:0day安全:软件漏洞分析技术 + 漏洞战争
本次对于 CVE-2010-2553 堆溢出漏洞的分析到此结束,如有错误,欢迎指证