CVE-2016-10191 FFmpeg RTMP Heap Buffer Overflow 漏洞分析及利用
一、前言
FFmpeg是一个著名的处理音视频的开源项目,使用者众多。2016年末 paulcher 发现FFmpeg三个堆溢出漏洞分别为CVE-2016-10190、CVE-2016-10191以及CVE-2016-10192。网上对CVE-2016-10190已经有了很多分析文章,但是CVE-2016-10191尚未有其他人分析过。本文详细分析了CVE-2016-10191,是学习漏洞挖掘以及利用一个非常不错的案例。
二、漏洞成因分析
在 RTMP协议中,最小的发送数据包的单位是一个 chunk。客户端和服务器会互相协商好发送给对方的 chunk 的最大大小,初始为 0x80 个字节。一个 RTMP Message 如果超出了Max chunk size, 就需要被拆分成多个 chunk 来发送。在 chunk 的 header 中会带有 Chunk Stream ID 字段(后面简称 CSID),用于对等端在收到 chunk 的时候重新组装成一个 Message,相同的CSID 的 chunk 是属于同一个 Message 的。
在每一个 Chunk 的 Message Header 部分都会有一个 Size 字段存储该 chunk 所属的 Message 的大小,按道理如果是同一个 Message 的 chunk 的话,那么 size 字段都应该是相同的。这次漏洞的起因是对于属于同一个 Message 的 Chunk的 size 字段没有校验前后是否一致,导致写入堆的时候缓冲区溢出。
漏洞发生在rtmppkt.c
文件中的rtmp_packet_read_one_chunk
函数中,漏洞相关部分的源代码如下
size = size - p->offset; //size 为 chunk 中提取的 size 字段
//没有检查前后 size 是否一致
toread = FFMIN(size, chunk_size);//控制 toread 的值
if (ffurl_read_complete(h, p->data + p->offset, toread) != toread) {
ff_rtmp_packet_destroy(p);
return AVERROR(EIO);
}
在 max chunk size 为0x80的前提下,如果前一个 chunk 的 size 为一个比较下的数值,如0xa0, 而后一个 chunk 的 size 为一个非常大的数值,如0x2000, 那么程序会分配一个0xa0大小的缓冲区用来存储整个 Message,第一次调用ffurl_read_complete函数会读取0x80个字节,放到缓冲区中,而第二次调用的时候也是读取0x80个字节,这就造成了缓冲区的溢出。
官方修补方案
非常简单,只要加入对前后两个 chunk 的 size 大小是否一致的判断就行了,如果不一致的话就报错,并且直接把前一个 chunk 给销毁掉。
+ if (prev_pkt[channel_id].read && size != prev_pkt[channel_id].size) {
+ av_log(NULL, AV_LOG_ERROR, "RTMP packet size mismatch %d != %d\n",
+ size,
+ prev_pkt[channel_id].size);
+ ff_rtmp_packet_destroy(&prev_pkt[channel_id]);
+ prev_pkt[channel_id].read = 0;
+ }
+
三、漏洞利用环境的搭建
漏洞利用的靶机环境
操作系统:Ubuntu 16.04 x64
FFmpeg 版本:3.2.1 (参照https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu编译, 需要把官方教程中提及的所有 encoder编译进去。)
官方的编译过程由于很多都是静态编译,在一定程度上降低了利用难度。
四、漏洞利用脚本的编写
首先要确定大致的利用思路,由于是堆溢出,而且是任意多个字节的,所以第一步是观察一下堆上有什么比较有趣的数据结构可以覆盖。堆上主要有一个RTMPPacket结构体的数组,每一个 RTMPPakcet 就对应一个 RTMP Message, RTMPPacket 的结构体定义是这样的:
/**
* structure for holding RTMP packets
*/
typedef struct RTMPPacket {
int channel_id; ///< RTMP channel ID (nothing to do with audio/video channels though)
RTMPPacketType type; ///< packet payload type
uint32_t timestamp; ///< packet full timestamp
uint32_t ts_field; ///< 24-bit timestamp or increment to the previous one, in milliseconds (latter only for media packets). Clipped to a maximum of 0xFFFFFF, indicating an extended timestamp field.
uint32_t extra; /