2017-01-13 13:31:46
来源:scarybeastsecurity.blogspot.com 作者:babyimonfire
阅读:1091次
点赞(0)
收藏
翻译:babyimonfire
预估稿费:260RMB
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
概览
gstreamer 0.10.x 播放器在播放NSF格式的音乐文件的时候,存在一个漏洞和单独的逻辑错误。两者结合,可以实现非常稳定的漏洞利用方法,并且能过绕过64位ASLR,DEP等等。所谓的稳定是因为该音乐播放器里提供一个图灵完备的“脚本语言”。NSF文件是任天堂游戏机中的音乐文件。有意思吧?那就继续往下看。。。
漏洞演示及受影响的发行版本
下图是触发该漏洞的一个截图。比较让人揪心的是,漏洞触发不需要用户打开恶意文件——只需要打开恶意文件所在位置,就可实现。下面有更多内容。你可以从exploit_ubuntu_12.04.5_xcalc.nsf下载这个文件。在上图中,文件被重命名为“time_bomb.mp3”,下面我们会解释为什么要这么做。
从文件名中可以看出,这个漏洞可以在Ubuntu 12.04.5下进行利用。这是一个较老但仍然有官方支持的发行版本。此外,在重现漏洞方面,该PoC可在未进行相关更新的Ubuntu 12.04.5下直接执行。如果你进行了全部更新,会安装一个新版本的glibc,而里面一些代码的偏移量发生了改变,因此该PoC一定会失效崩溃,但仍然可以通过编写新的PoC代码实现适配任意版本的glibc。这项工作就留给读者当做练习了。
漏洞存在于gstreamer-0.10版本的一个音频解码器libgstnsf.so里。Ubuntu 12.04使用gstreamer-0.10处理所有音频处理需求。而Ubuntu 14.04显然也受到该漏洞的影响,因为gstreamer-0.10是默认安装的,但大部分多媒体软件使用同样被安装的gstreamer-1.0进行相关的处理。目前还不清楚Ubuntu 14.04中何时使用存在漏洞的gstreamer-0.10进行媒体处理的具体情况。Ubuntu 16.04默认只安装了gstreamer-1.0,因此不受此漏洞的影响。
这个漏洞存在于所谓的“默认”安装。在Ubuntu系统安装时,会有出现一个提示信息:“hey, do you want mp3s to work?”,当然,正确答案是“yes”。随后许多额外的包,包括gstreamer0.10-plugins-bad就被安装了,而其中就包含了libgstnsf.so。
等等,你说什么?0day?还有PoC?
是的,0day。
作为一个学习实验,我未来公开的大部分漏洞都会是0day。我在参与所谓的“协作公开”中获得了大量经验,即让收到漏洞预警的厂商花费他们需要的时长进行修复(为了让苹果公司修复一个Safari浏览器漏洞,我曾经等了一年!)而在“全面公开”方面,即厂商和公众同时获取到漏洞的细节,我明显缺乏更多的经验。声明一点,我非常确定在“协作公开”和“全面公开”上的正确平衡就是在两者之间做出妥协。Project Zero的90天期限策略似乎能较好地实现这个妥协,并且有很多的数据可以支持这一策略。
不要担心,这个0day并不严重,只影响到非常老的Linux发行版本(见上述)。这个0day更多的是好玩而不是影响。未来的0day影响可能才会更加广泛。;-)
关于0day的哲学问题
如果补丁和0day同时发布,那还能算是0day么?下面就是针对Ubuntu 12.04的补丁:
sudorm/usr/lib/x86_64-linux-gnu/gstreamer-0.10/libgstnsf.so 第一眼的话,这个“补丁”似乎是直接删掉了这个功能,其实不是。你的NSF文件仍然能够播放。WTF?你能相信Ubuntu 12和14为了播放NSF文件都装载了两个不同的代码库么?为一个很少见的格式配备的那么多播放器似乎显得多余。第二个NSF播放器基于libgme,并且没有像第一个播放器那样存在该漏洞。攻击面
这个漏洞利用了gstreamer-0.10中播放NSF音乐文件的插件。这些NSF音乐文件跟其他大多数能在系统上播放的音乐文件不一样。典型的音乐文件是基于数学计算进行压缩和解码的,但是NSF音乐文件却是通过实时模拟任天堂(NES)CPU和音频硬件来播放的,厉害了!gstreamer插件创建了一个虚拟的6502 CPU硬件环境,然后通过在一段时间里运行一些6502指令,再到虚拟出的音频硬件寄存器中找到指令运行的结果,基于这个方法,实现音频的播放。如果你对真正播放一个NSF文件感兴趣的话,可以下载这个文件:cv2.nsf,这是游戏恶魔城2中的音频文件,你还可以通过谷歌“nsf music files”等关键字,很容易就能找出类似的音频文件。如果你的桌面版Linux支持NSF文件,你就应该能通过执行一些命令实现播放,例如“totem cv2.nsf”。(如果不支持的话,你的Linux系统可能会自动安装一个合适的插件进行播放。)这个文件只有17264字节,对于文件中包含的音频数量来说,文件占用空间显得太小了,不足以存放那些内容,但是这个大小却足以容纳一些小程序通过向基本的NES硬件发出序列请求,从而发出一系列简单的声音。
实现这个漏洞利用,有多种精巧而不同的方法:
通过邮件附件形式发送。如果目标用户下载并打开了附件,就遭到了该攻击。注意,为了让恶意文件正常执行,你可能需要重命名exploit.nsf为exploit.mp3。因为大多数桌面版Linux系统无法识别NSF文件类型,但却会把MP3文件的字节序列传递给媒体播放器。大部分基于gstreamer的播放器都会忽略文件的后缀名,而使用自动检测出的文件格式选择合适的解码器。
依靠部分路过式下载。利用谷歌Chrome浏览器文件下载的UX(用户体验),当访问一个陷阱网页时,有可能将该恶意文件直接转储到目标用户的下载文件夹中。而当之后通过文件管理器(例如nautilus)浏览到该下载文件夹时,系统会自动尝试为已知后缀名的文件生成缩略图(所以,需要将恶意NSF文件后缀名改为.mp3)。而为恶意NSF文件生成缩略图时,会触发该漏洞。
依靠完整路过式下载。还是利用谷歌Chrome浏览器的下载UX,有一种方法可以利用完整的路过式下载实现漏洞利用,具体方法会在另外一篇博文中描述。
基于USB设备进行。打开USB存储设备时,仍然会自动生成已知后缀名文件的缩略图,同上。
6502 CPU介绍和任天堂ROM加载以及分页崩溃过程
6502 CPU本身是一个传奇,同时被大量也堪称传奇的系统所使用,如任天堂,Commodore 64,BBC Micro等。这是一个8位的CPU,但是有着16位寻址能力,并且拥有64kB的地址空间。在任天堂应用中,高32kB的地址空间(0x8000 - 0xffff)保留给了ROM,即插入的游戏卡中的只读数据。现在有个有趣的问题:如果你想制作一个大于32kB的游戏该怎么办?
例如,你有一个16关的游戏,每一关都有16kB大小的视频和音频数据。那么32kB的空间是不可能放得下的。为了解决这个问题,便出现了“库(bank)”和“库切换(banks switching)”的概念。所谓库,就是ROM上一块对齐、连续的4kB区域。一共有8个库,分布在6502地址0x8000 - 0xffff上。每个库都可以映射为游戏卡ROM(可以远大于32kB)上一个连续、对齐的4kB区域。在运行时,任天堂程序可以对魔法内存(magic memory)位置(0x5ff8 - 0x5fff)进行写操作。这一位置包含控制将ROM中哪一部分映射到哪个库中的硬件寄存器。
例如:如果6502 CPU向0x5ff9写入数值10,那么6502内存位置0x9000 - 0x9fff就会被映射为游戏卡ROM中的索引(10*4096)处。
漏洞
1:在映射到6502内存和库切换时,缺少对ROM大小的检查(此处没有CVE,可以当做是CESA-2016-0001。)
在提到的ROM映射上,几乎是完全没有边界检查。不仅是初始加载ROM时是这样,后续的一系列库切换操作同样没有边界检查。所有对ROM映射的处理都在gst-plugins-bad/gst/nsf.c,包括:
nsf_bankswitch(uint32address,uint8value) { ... cpu_page=address&0x0F; roffset=-(cur_nsf->load_addr&0x0FFF)+((int)value<<12); offset=cur_nsf->data+roffset; ... cur_nsf->cpu->mem_page[cpu_page]=offset; …上面的代码片段中,cur_nsf->data指向实际的ROM数据即音频文件的内容。文件格式非常简单:128字节的头(以“NESM”四个字母开始),剩下的就是ROM。所以,举个例子,假如你有一个200字节的文件,那么就是128字节的头和72字节的ROM。通过单个malloc()函数,即可将所有ROM的数据都保存在主机模拟器的堆中。可以看出,offset指针可以指向ROM堆内存中的任意位置,而没有对任何类型的长度进行检查!
再举个更具体的例子:我们对200字节的文件进行最简单的ROM加载,虚拟ROM加载地址会是0x8000,而加载代码仍会多次调用nsf_bankswitch(),对应的参数分别是:地址(address)从0x5ff8 - 0x5fff,库索引(value)从0 - 7。这会导致6502虚拟地址0x8000被映射为nsf->data + 0,0x9000被映射为nsf->data + 4096,……一直到0xf000被映射为nsf->data + (7 * 4096)。所以即使在这个非常简单的例子中,从6502模拟器中线性地读取0x8000 - 0xffff就会导致实际读取到72字节的ROM数据和32696字节的越界堆数据。
但这只是一个越界读取,因为模拟器中的虚拟地址0x8000 - 0xffff是只读的。在模拟器下,一个越界(OOB)读取漏洞并不是特别严重。或许可以将其用作绕过ASLR的工具,但任何从主机堆中读取到的敏感数据都只能用来产生声音。这个模拟器没有任何高级功能,例如通过网络连接传出堆数据的能力。最严重的情况,也就是该模拟器运行在一个提供音频格式转换的网络服务器上,攻击者可以将部分越界读取到的堆的内容输出到转换文件中的音频内容,从而获取到服务器中的堆数据。
但是,这个特殊的模拟器中的另外一个逻辑处理错误加重了事态的严重性:
2:能够通过加载或者库切换操作,使ROM映射到可写的内存位置(本身或许算不上一个真正的漏洞;no identified assigned.)
我其他的任天堂音乐播放器上发现,都不允许在0x8000地址以下进行ROM加载或者库切换。但是这个特殊的播放器,通过在文件头中写入低于0x8000的ROM加载地址,或者通过向库寄存器0x5ff6或0x5ff7中写入(其他的模拟器甚至没有0x5ff6或0x5ff7这么低的库寄存器),却可以实现在0x8000地址以下进行写入:
staticnes6502_memwritedefault_writehandler[]={ {0x0800,0x1FFF,write_mirrored_ram}, {0x4000,0x4017,apu_write}, {0x5FF6,0x5FFF,nsf_bankswitch}, {(uint32)-1,(uint32)-1,NULL} };例如,向0x5ff6处写入0x00,会使得ROM中的前4096字节在6502虚拟地址0x6000处映射为可读可写。在我们200字节的文件样例中,这个操作意味着通过向虚拟地址0x6048写入一串0x41,会导致这一串0x41被越界写到对应的主机上模拟器的堆中。
可以发现,现在我们对主机上模拟器的堆,有了很多读写方面的控制,一些经验丰富的漏洞挖掘人员或许已经意识到一次漏洞利用已经可以确定了。
漏洞利用:概览
下面是在okteta(一个16进制编辑器)中查看的漏洞利用文件:图上可以看到的是一个完整的漏洞利用文件,文件被压缩到仅仅416字节。途中标出了三种颜色的线条,表示漏洞利用文件中的不同部分。
蓝色,表示128字节的文件头。文件头大部分的字节都与漏洞利用不相关,因此实际上,一些高明的漏洞利用技术会把一些payload压缩到文件头中,以增大空间利用率。你也可以尝试将这种方法应用到这个漏洞中来,使用尽可能少的字节实现相同的功能,这将会是一个很有意思的挑战:)而文件头中重要的字段列举如下:
位于0x08偏移的0x8000(使用小端存储,下面也是)。表示ROM虚拟加载地址。
位于0x0A偏移的0x8070。表示6502初始例程(调用一次)的虚拟地址。
位于0x0C偏移的0x80a0。表示6502每帧例程的虚拟地址。
位于0x6E偏移的0x41A1。表示帧对时。为了确保音频引擎正常工作,需要保留该数值。
橘色,表示元数据,位于ROM的开始,紧随文件头之后。这些元数据会被加载到虚拟地址0x8000处,可以由6502程序获取和使用。元数据中包含字符串“xcalc”,即我们的payload,一个用来在堆中搜索定位的常量(使漏洞利用更可靠),和一个向堆中执行读取、增加、写入的操作表,以实现漏洞利用。
绿色,表示真正的6502操作指令。漏洞利用会通过一个使用6502汇编语言编写的程序继续进行。由于上述的漏洞细节,音频播放模拟器会在主机上模拟器的堆中模拟这些指令。
漏洞利用:细节
到底漏洞利用文件是怎么做到在如此小的体积下实现弹出一个计算器的呢?文件大小是416字节:128字节的文件头和288字节的ROM,其中ROM被映射到虚拟6502地址0x8000。ROM中包含一些元数据和一些6502指令。为了探索6502以及“编译”6502汇编代码,推荐这个网站:Easy 6502。这个网页声称“6502 is fun!”——我也这么认为。详细注释指令操作的6502汇编代码可以从这里找到:asm_final_main.asm。还有一些附加小程序:asm_final_init.asm,asm_final_adder.asm。
漏洞利用后续步骤:
1:在堆中定位nes6502_context类型的重要元数据对象
因为上面提到的漏洞,任何从0x8120 - 0xffff的读操作都会导致越界读取,所以我们因此可以一直进行越界读取,直到找到主机的堆中对应nes6502_context的对象,nes6502_context的定义如下:
typedefstruct { uint8*mem_page[NES6502_NUMBANKS];/*memorypagepointers*/ ... nes6502_memread*read_handler; nes6502_memwrite*write_handler; intdma_cycles; uint32pc_reg; uint8a_reg,p_reg,x_reg,y_reg,s_reg; uint8int_pending; }nes6502_context;为什么非要定位这个对象?有两个原因。第一,它控制6502虚拟内存如何访问到主机堆的内存映射。通过控制映射,我们能够获取到主机进程的虚拟内存上任何位置的读写权限。第二,它包含指向BSS段的指针。而定位BSS段在之后的漏洞利用中很重要。
2:重新映射6502可读写的虚拟地址0x6000,使其指向nes6502_context::mem_page[6]下面的代码是通过写入魔法硬件寄存器,将0x6000映射到越界ROM外:
;一些非常简单的计算和内存库的重映射。 ;匹配的地址存储在0x02。例如:0x91b0 ;从匹配的地址减去0x60。 ;这将更早地索引到堆中的元数据对象。 ;即索引到一个指向65020x6xxxRAM的堆指针 ;减去0x8000,得到距离ROM基址的偏移量。 LDA$02 SEC SBC#$60 STA$02 LDA$03 SBC#$80 STA$03 ;现在,0x02包含了结果。即与例子相对应的0x1160。 ;进行移位操作,获取ROM对应的库号,即最高位的值。 ;每个库的大小都是0x1000 LSR LSR LSR LSR ;在这个例子中,我们的库号是1。 ;把1写入魔法硬件寄存器0x5ff6中。 ;将0x6xxx处的RAM映射为ROM中0x1000的位置。 ;从而可以实现对主机的堆进行越界操作。:-) ;注意,0x6xxx是可写的,而0x8xxx是只读的,所以我们需要这么干。 ;假设0x6xxx地址空间中的偏移量0x160是堆指针, ;那么利用这个堆指针就可以从6502地址0x6160对堆进行读写操作了。 STA$5ff6如果你对6502还不是很熟,但愿你能理解这些简洁的操作指令。下面是一些注意事项:
没有指定次数的位移操作。因此,4个LSR(逻辑右移)操作,等效于C语言中的 >> 4。
8位的处理器,不存在16位的寄存器。所以一次简单的16位运算需要被分成两半进行,同时还要处理标志位(SBC为带借位减法)。
当相应ROM上的库被映射为可写后,就要进行一些正常的计算了——将库偏移量增加到对应主机堆指针nes6502_context::mem_page[6]处。这是一个非常精确的内存损坏漏洞,需要等到下一帧才会生效,保证0x6000能精确的指向nes6502_context::mem_page[6],即我们要映射的任何库偏移量的位置。3:在每帧一次的循环中,进行一系列读取/添加/写入操作
有了6502虚拟地址0x6000指向nes6502_context::mem_page[6]的条件之后,我们就可以开始使用6502操作指令精确读写全部主机堆(栈/BSS/或者是任何可以找到的指针)了。如果我们修改mem_page数组,那么到下一帧访问6502内存时才会生效,所以我们每一帧只简单的做一次内存修改。读取/添加/写入的一些列操作位于ROM中偏移量为0x20的地方,每个操作都是8字节,如第一个操作:
50600860606fffff这个操作代表,从0x6050读取8字节,加上0xffff6f60(符号扩展,这里相当于减法),然后写入到虚拟地址0x6008处。
4:计算libgstnsf.so中BSS段的地址
nes6502_context::read_handler指向BSS中的一个对象,是一个位于BSS段开头固定位置的值,而这个值现在已经存储在虚拟地址0x6050中了。我们计算出BSS的起始位置,然后写入到虚拟地址0x6008处,即nes6502_context::mem_page[7]的位置。也就是说,我们把BSS映射到了一个可读可写的6502虚拟地址0x7000处。5:修改memset()的GOT表
在GOT表偏移量0xf8的位置,是memset()的函数指针。这个指针指向glibc,而指针现在被映射到虚拟地址0x70f8处。你知道glic中还有什么相对偏移量固定的函数指针么?system()。通过增加一个固定值到GOT表中的memset()函数指针,我们可以实现后续调用memset()函数时变成调用system()函数。
6:将nes6502_context::read_handler对象映射到0x7000处
下面是read_handler的定义;read_handler指针指向一个如下的数组:
typedefstruct { uint32min_range,max_range; uint8(*read_func)(uint32address); }nes6502_memread;下面是数组中的一些记录:
staticnes6502_memreaddefault_readhandler[]={ {0x0800,0x1FFF,read_mirrored_ram}, {0x4000,0x4017,apu_read}, {(uint32)-1,(uint32)-1,NULL} };如你所见,这个对象中包含函数指针,很有用处。另外,那些对6502某个虚拟地址进行内存访问操作中会调用的函数指针也很有用。
7:更改apu_read()的函数指针
通过访问虚拟地址0x7018,我们现在访问到了read_handler BSS对象的0x18偏移处,存储着读取0x4000 - 0x4017的apu_read()函数指针。我们向该函数指针增加一些偏移量(0x1d0),从而使函数指针指向改变到了apu_reset()函数。你马上就能知道为什么要这么做了!
8:计算BSS变量apu的地址,再次利用nes6502_context::read_handler中的一个固定相对偏移量
apu定义如下:
/*pointertoactiveAPU*/ staticapu_t*apu;通过计算,使虚拟地址0x7000指向apu的值。
9:将apu指针的值复制到内存中的库映射,以便我们间接引用虚拟地址0x7000上的apu对象
这里只需要一定程度的间接指针跟随,因为BSS的值只是一个指向堆中实际对象的指针。
10:将字符串“xcalc”写入到apu对象中
apu对象的大小还是比较大的:
typedefstructapu_s { rectangle_trectangle[2]; triangle_ttriangle; noise_tnoise; dmc_tdmc; uint8enable_reg; apudata_tqueue[APUQUEUE_SIZE]; …通过向0x70f0进行写入,我们就可以实现对apu结构中偏移量为0xf0的地方进行写入,即queue的缓冲区域。我们将字符串“xcalc”写入到此处。
11:从0x4000地址处进行读取
然后就弹出了一个计算器!难道是黑魔法?当然不是,我们之前的步骤中的一系列小心的操作造成了这个计算器的弹出。下面是本步骤背后的一些执行顺序:
1. 6502从0x4000开始读取。
2. 这个特殊的内存地址,本来是应该调用apu_read()函数指针来处理读取访问的。
3. 然而,调用的却是apu_reset(),因为我们之前已经更改了对应的函数指针。
4. apu_reset()包含这句代码:memset (&apu->queue, 0, APUQUEUE_SIZE * sizeof (apudata_t));
5. 但是,我们将memset()函数的GOT表指向了system()函数,并且向apu->queue中写入了字符串“xcalc”。
6. 因此,执行的是system("xcalc"),然后就弹出了计算器。
一些漏洞利用的附加说明:
在如下一些程序中,该漏洞利用文件一样可以进行工作:
Totem
Rhythmbox(效果太好,以至于会弹出两个计算器)
gst-launch-0.10
nautilus (有可能是启动了一个子进程——totem-video-thumbnailer)
我们不需要考虑堆布局的变化。这些代码能扫描堆中可利用的元数据对象,而不是依靠一个固定的偏移量来定位,因而提供了更大的可靠性。聪明的读者可能会注意,对堆的扫描只能向前进行,并且只能扫描大小约32kB的空间。因此,如果堆抖动导致所有重要的元数据对象都被分配到了ROM数据之前怎么办?这的确有可能,但是在这个例子中并不会造成太大的麻烦。因为NSF解码器运行在一个全新的线程,通常都会分配一个新的堆上,从而在堆布局上较为得当。这样,元数据对象会临时分配到ROM数据之后,因此,通常在堆中也会被放到ROM数据之后。即便如此,如果ROM数据足够大的话,是有可能被放到元数据对象之后的(确切的说,是由于固定的堆大小导致的)。
最后一点关于堆布局的说明,如果需要的话,我们是能做一些堆“修饰”的。例如除了攻击者控制的ROM大小,还有一些不固定长度的文件头字符串(音乐标题等)会被分配到堆上。另外,gstreamer中格式检测的代码非常复杂,有可能提供更多的机会去控制堆的状态。
本文由 安全客 翻译,转载请注明“转自安全客”,并附上链接。
原文链接: https://scarybeastsecurity.blogspot.com/2016/11/0day-exploit-compromising-linux-desktop.html