*严正声明:本文仅限于技术讨论与分享,严禁用于非法途径
下面的所有分析都是在Firefox 59.0 32位上进行的。由于笔者是刚入门的小白,水平有限,文章有错误或者写法不当的地方,请各位师傅斧正。
这是一个firefox的漏洞,位于libvorbis库中,在音频合成过程的深层次位置,想要触发它只能由具有residue 1结构的音频文件。
Vorbis是一种音频压缩格式,它的相关数据会被封装到一个OGG文件中;OGG则是一种多媒体文件格式。
下图是用010Editor中的ogg.bt模板读取出来的。可以看到这个ogg文件有3个页。后面的分析,都将使用这个下图这个文件,姑且取名为TestOgg;下面的分析过程都是建立在一个正常的ogg样本之上的,这里就是TestOgg。
(更详细的ogg vorbis格式将在下面介绍)
系统:Windows7 32位
工具:Visualstudio 2013,一个正常的ogg vorbis音频文件。
因为需要使用到OggVorbis音频文件,而这个文件的格式又比较陌生。那么就需要一个能够帮助我们解析这个文件的工具。去官网发现有一些打包好的工程,和一些编译好的EXE文件。
这个名叫ogginfo .exe的文件就可以静态解析Ogg Vorbis文件
然后下载这些工程,在本地自行配置环境,编译一下ogginfo,成功之后就可以打断点进行调试一步一步分析了。
当然,这个编译过程可能不会成功,会报错,想要解决也行,这里我直接写下我最后成功的操作:
0.官网下载libogg-1.3.3,libvorbis-1.3.5,然后从某hub上下载的vorbis-tools-master。因为从官网下载的这个tools工程有问题,而且没有解决掉。
1.把libogg-1.3.3的include / ogg文件夹,和libvorbis-1.3.5下include / vorbis文件夹放到tools的include下面。
2.编译 libogg_static,出现下面问题,如下解决一下就行。
3.编译vorbis_static,先把libogg-1.3.3的include / ogg文件夹放到include下。成功后得到libvorbis_static.lib,改名为libvorbis.lib即可。
4.然后打开tools工程,把刚刚编译好的libogg_static.lib,libvorbis.lib的路径添加进去。
编译一下ogginfo,成功。
5. 将ogginfo设置为Startup Project,然后给它随便一个ogg文件作为参数就可以进行调试,我使用的TestOgg只有3个ogg页。
6.调试中比较关键的一个函数就是:oggpack_read
按F11跟进这个函数的时候就会提示你选择一个framing.c文件,选择如下这个src目录就行。
那么在解析vorbis的时候关键的函数是:vorbis_synthesis_headerin:
这里会先解析pack的类型,然后按类型来一一解析其中的数据,下面构造poc的时候会详细说明这些结构。
到此静态分析的环境搭建完成了;下面的源码分析和调试都是在这个环境搭建完成的基础上进行的,但是主要目的是帮助构造poc。
漏洞的具体位置在vorbis_book_decodev_add()这个函数中,在工程中的具体位置如下:
源码如下:
问题出在高亮部分那段for循环。这个for循环的条件是j <book->dim,里面的操作是把t数组赋给a数组;而a数组的下标则是i,在上一个for循环可以看到i < n。那么,这就存在当book->dim的值大于n时,t数组对a数组的赋值就会超过a数组的正常范围。a数组就在此时越界。
那么,a数组是什么,t数组又是什么?
从图上可以看到a数组是作为一个参数被传递进来的,而t数组是则是通过计算得来的
那么这里尝试寻找a数组,看看是从哪里来。
在源码中搜索漏洞函数名字,找到如下的调用,这个叫_01inverse的函数是漏洞函数的上一层。
那么继续寻找_01inverse,来到下面的位置:
红框标出的地方,就是漏洞函数的在这一层定义的结构。需要注意的是,目前要关注的数据是a数组,反应在这里就是float *这个位置的数据。在这个_01inverse的函数里,往下看可以发现下面这一段调用:
float *位置的数据传入的是in[ j] + offset,offset在上面计算了,那现在就是去找in[ j ]。in [ ]这个数组回头看 _01inverse的函数头部:
in[ ]是在第三个参数的位置。那么再回到res1_inverse:
in[ ]虽然在这里有赋值的过程,但不是我们要找的,这里的赋值也只是in自己给自己的数据在赋值,没有实质上的变化,需要找到是in[ ]是从哪里来的。
接下来去找res0_inverse:
residue1_exportbundle:
这次去寻找_residue_P的时候,会找到多个位置。我们要找的是_residue_P -> inverse这样的调用。所以来到如下的位置:
pcmbundle参数就是in[ ],从代码里去搜索pcmbundle。
pcmbundle在这里被分配了空间,它就相当于是in[ ];但是这还没有完。下面有另一处赋值:
这里的pcmbundle[ ]相当于in[ ],所以in[] =vb->pcm[ ]。继续去搜索vb->pcm:
vb->pcm也会搜索到很多地方,但是下图才是正确的位置。因为根据漏洞函数的触发流程只可以知道_mapping_P是会被调用到的一层。
vb->pcm[ ]的大小由vb->pcmend*sizeof(*vb->pcm[i])决定,vb->pcmend的值由ci->blocksizes[vb->W],而ci->blocksizes的数据来自vorbis的第一个头部Identification header中。
所以a数组的大小是可控的。而分配内存的函数_vorbis_block_alloc实际上是_ogg_malloc :
回溯a[ ]的过程,再进一步分析,也就搞清楚出了漏洞触发的过程,整理如下:
t [ ]由book->valuelist,entry,book->dim的值计算得到。
由ZDI的公告可以知道book->dim的值来自文件中某一位置的数据。而book->valuelist,entry并不清楚。这里将不回溯去寻找t [ ],只需要明白一点就是通过修改book->dim可以影响到t[ ]的数据,且触发的漏洞的一个条件是book->dim > 8。
在了解漏洞的具体情况之后,下面就该来分析ogg vorbis的格式,构造能触发漏洞的音频文件了。去查看官方文档的说明,可以发现文件的格式不是按byte对齐的,如果不借助前面搭建的环境想要一步一步分析清楚是相当麻烦的一件事。
首先,对ogg文件格式进行介绍。
Ogg是以页为单位的,每一个页都有如下的固定结构:
1.Capture_pattern:页标识,是ASCII字符,既:OggS,4字节大小。
2.Stream_structure_version:版本ID,默认为0,1字节大小。
3.header_type_flag:当前页的类型,1字节大小。
可以是:
0×01:表示本页与前一页属于同一个逻辑比特流的同一个Packet;若没有设置,则是一个新的Packet。
0×02:表示本页是逻辑比特流的第一页bos;若没有设置则不是。
0×04:表示本页是逻辑比特流的最后一页eos;若没有设置则不是。
4.granule position:编码的相关参数,8字节大小,可以设置为全0。
5.serial number:当前页的流ID,4字节。
6.page sequence:页面序列号,用来判断页面有无丢失,4字节大小。
7.page checksum:包含头部的页面校验和,4字节。
8.page_segments:segment_table中出现的个数,最大为255,1字节。
9.segmentLen: 记录每个segment_table长度的数组,它是一个数组,大小由segmentable的个数决定。假设只有一个segment_table且长度为0x1E。那该位置就是1字节,且数据是1E。
10.segment_table:段表,存放数据的地方,大小在0-255字节。
需要注意一下的是:page checksum,这个需要根据文件数据的改动而改,不然文件就是一个无法识别的错误文件。
根据vorbis的标准可知,它有3个标识头,头部之后的所有数据包都是音频数据包。
标识头依次是:identification header,comments header,setup header。
这3个头部都还有一个公共的头:Common header,结构如下:
1) [packet_type]: 包类型, 8字节大小;
01: 表示 identification header
03: 表示 comments header
05: 表示 setup header
2) 0×76,0x6f,0×72,0×62,0×69,0×73:ASCII 字符,既:vorbis,6字节。
4.2.1 Identification header
官方的介绍如下:
1)[vorbis_version]=read32bitsasunsignedinteger
2)[audio_channels]=read8bitintegerasunsigned
3)[audio_sample_rate]=read32bitsasunsignedinteger
4)[bitrate_maximum]=read32bitsassignedinteger
5)[bitrate_nominal]=read32bitsassignedinteger
6)[bitrate_minimum]=read32bitsassignedinteger
7)[blocksize_0]=2exponent(read4bitsasunsignedinteger)
8)[blocksize_1]=2exponent(read4bitsasunsignedinteger)
9)[framing_flag]=readonebit
我用一个实例来对上面的参数进行说明,如下图:
这里可以看到图上有2个OggS头的标识。
图中的蓝色部分:01 76 6F …. 00 B8 01;就是common header + identification header的数据。
packet_type =01
76 6F 72 62 69 73 =vorbis
vorbis_version =00 00 00 00
audio_channels =02
audio_sample_rate =44AC 00 00
bitrate_maximum =00 00 00 00
bitrate_nominal =70 11 01 00
bitrate_minimum =00 00 00 00
blocksize[0–1] =B8
framing_flag =01
这之后就是下一个Ogg的页,这一页可以看到packet_type = 03,说明接着是comments header。
4.2.2 Comments header
同样给出官方的说明:
1)[vendor\_length]=readanunsignedintegerof32bits
2)[vendor\_string]=readaUTF-8vectoras[vendor\_length]octets
3)[user\_comment\_list\_length]=readanunsignedintegerof32bits
4)iterate[user\_comment\_list\_length]times{
[length]=readanunsignedintegerof32bits
thisiteration’susercomment=readaUTF-8vectoras[length]octets
}
5)[framing\_bit]=readasinglebitasBoolean
6)if([framing\_bit]unsetorend-of-packet)thenERROR
7)done.
继续用上面的例子来说明:
这已经是Ogg第二个页了,这里可以看到SegmentLen的长度是14,说明它的大小就是14字节。
蓝色的部分就是common header + comments header:
1、packet_type =01
2、766F 72 62 69 73 =vorbis
3、vendor\_length =1D 00 00 00 表示下面的字符串长度:0x1D= 29 字节
4、vendor\_string =00 58 69 70 … 30 36 32 32 共29字节,表示制作软件信息的字符串。
5、user\_comment\_list\_length=02 00 00 00表示用户注释字符串的个数为 2 ,也就是下面讲有2个字符串。
[user length ]=2B 00 00 00 表示第一个用户注释字符串长度是0x2B = 43。
usercomment:3D43 6F 70 … 6A 65 63 74这一段就是第一个用户注释了。特别的:3D 对应”=”,这个等号用于终止字段名;这里没有字段名。
[user length ]=14 00 00 00表示第二个用户注释字符串长度是0×14 = 20。
usercomment:7469 74 6C … 74 74 65 72第二个用户注释;同理,这里的字段名是“title”;
6、framing\_bit =011字节的Boolean类型数据; 若没有设置则会出错。
到这里Comments header就结束了。但是接下来数据可以看到是05,说明紧接着就是setupheader的结构。
4.2.3 Setup header
这个结构是最关键的部分,也是构造的难点所在。ZDI上所说的type 1 residue encoding结构就包含在这里。
这段数据比较复杂,就不能一位一位来进行说明,先给出这个部分的整体结构
下面将根据我们搭建的ogginfo项目来分析Setup header,同时会对照图中的数据一步一步说明。
1、将断点打在_vorbis_unpack_books,项目中解析的源代码如下图:
这里的截图由于函数过长不能完全显示出来,下面还剩下一个modesettings的结构。
2、打好断点,开始调试。
从图中可以看到程序走到了这个位置,且前面提到的blocksizes[ ]已经被计算出来了,注意这个值是可以影响a[ ]大小的。至于blocksizes[ ]的值可以在第一个vorbis头的解析函数中去查看是如何计算的,如下图:
接着回到刚刚断点的位置:
F11跟进oggpack_read函数:
在内存中查看对应的位置0x00140E83,这里的数据就是Setup header除去vorbis头部的后的部分了;注意红框的bits,这个参数决定了这里的数据从0x00140E83的位置读取多少位;图中bits =8,说明读取1个字节,那么读取的数据就是0x00140E83的第一个字节:0×22。
(前面提到这个oggpack_read函数很重要的原因就是ogg文件的读取不是按字节对齐的;它存在跨字节读取数据,这就使得分析起来比较复杂,所以我才搭建这个project来帮助分析。)
然后下面进行计算,具体计算可自行分析,不再过多介绍。
最终得到的:books = 34 + 1 = 35;
最后有个加一,所以cooks的值不可能设置为0。也就意味着可以把 0×22 改写为 0×00 。
为什么要改写为0?
因为我的目的是构造poc,那么这些结构的数量在保证正确的情况下,数量更小,方便对数据进行组织,出错也更容易修改,且构造出的poc文件更小。
然后跟进vorbis_staticbook_unpack函数:
这里就可以看到s->dim和s->entries的值是从文件什么地方读取的,dim值就是book->dim,但是这个entries不是t[ ]中的entry。
这里为了更好的说明,在windbg下调试最终构造出的poc可以看到:
此时运行到imul edx, eax
在我构造的文件中dim = 0×48,entries = 8。
这里eax = 48存放的就是dim值,edx = 0是entry的值。
3、residue结构
上面说过,漏洞文件出在type 1 residue encoding,那我们略过中间的time,floor结构的构造部分,这里我略过不代表不去构造,而是限于篇幅不再细细分析。
直接来到解析residue结构的位置:
这里有个residue_type的类型判断,所以构造的时候必须是“1”类型;后面可以跟进res0_unpack,这里面就是解析residue的具体结构了。
同理,下面的结构也就不再分析。
到此,对于POC如何构造就结束了。最后提示几点:
(1)、构造poc之前,我用了一个只有3个ogg页的正常的ogg文件来作为基础进行改动。
(2)、需要改动的部分实际上全在ogg第二页。
(3)、改动数据之后要记得修改checksum的值,这个checksum值不是常规的crc32计算出来的,但是我找到了它的crc32计算的源码,和编译好的exe程序。
(4)、在构造setup_header时,codebooks,time ,floor…等这些结构不一定是相邻的。比如:codebooks结构之后是:一部分其他数据+ t[ ]的数据,t[ ]数据之后才是time ,floor的结构。
(5)、在上面静态分析中,我们不可能走到漏洞函数的位置去,也就是说是无法调试漏洞函数。因为漏洞的触发是在音频合成的过程中,这是个动态的过程,上面只是静态的分析数据,也就是判断文件是不是一个正常的ogg vorbis文件而已。
虽然这样做十分的花时间,这也是一个比较笨的办法。不过好处就是清清楚楚的知道漏洞的中每个参数从文件中的什么位置去取得的,进行了怎么样的计算,改动什么位置的数据可以达到想要的目的。最终的构造出的poc也很小。
这一块篇幅问题,就省略了。
由于沙盒机制的存在,所以先关闭firefox的沙盒。
然后用xammp模拟一下http访问的情况,执行exp,弹出cmd。
1.EXP的稳定性
上面所有的情况,都是在我测试的环境中。EXP中用了多次堆喷,在自己的环境中也大概只有50%左右的成功率。
2.alert标签的问题。
测试阶段会加上的alert标签。alert标签去掉之后,exp执行的过程有点不符合预期。查看内存的时候堆喷确实成功喷到了,但搜索内存的代码会出现找不到相关特征值的情况。
Alert标签加上才成功,我有以下的猜想:堆喷中用到了大循环,JS引擎可能会做优化,alert可能会影响优化过程。另一种想法:firefox可能能区分js代码中的dom部分与js部分,然后对这些进行异步执行。
*本文原创作者:Obiit ,本文属于FreeBuf原创奖励计划,未经许可禁止转载