확장 가능한 parallel fuzzing 인프라 구축 및 성능 비교 분석 (3-2) vlc Fuzz 및 결론

OOSUlZ·2023년 2월 24일
0

2) VLC

VLC Media Player는 자유-오픈 소스 포터블 크로스 플랫폼 미디어 플레이어 소프트웨어이다. AFL++: Combining Incremental Steps of Fuzzing Research 논문을 참조하여 공식적으로 CVE-LIST에 등재가 된 버그를 조사해보고 Fuzz를 활용하여 찾는 과정을 직접 구현해보았다.
이 중, CVE-2019-14533CVE-2019-14438는 어떤 버그인지 조사해보았고, CVE-2019-14776을 직접 구현해보려 했다

(0) BUG Type를 이해하기 위한 사전 조사

메모리(Memory)

메모리는 크게 4개의 영역으로 구성된다.
  1. 코드 영역: 실행할 프로그램의 코드가 저장되는 영역이다.
  2. 데이터 영역: global과 static변수들이 저장되는 영역이다. 프로그램 시작시 할당되고 종료시 소멸되는 영역이다.
  3. Heap 영역: 사용자가 메모리를 동적으로 할당하는 영역이다. 런타임에 그 크기가 결정되며 사용자가 직접 관리를 해줘야 한다. C언어의 malloc, calloc, realloc, free를 사용하면 접근하게 되는 영역이다.
  4. Stack 영역: 함수 호출과 관련한 영역이다. 컴파일시 그 크기가 결정되며, 함수가 호출되면, 각 함수와 그 안에 포함되어 있는 지역 변수와 매개변수들이 하나의 블럭 형태로(이걸 stack frame이라고 한다.) stack에 push된다.

Stack Overflow

 함수가 저장되는 stack 영역에 잘못된 재귀의 형태로 인해 함수가 무한히 호출될 경우, 각 함수의 호출이 stack에 무한히 쌓이게 된다. 어느순간 원래 할당되었던 크기를 넘어가게 되면서 의도하지 않았던 위치에 데이터를 덮어쓰게 되는 문제가 생길 수 있다. 이를 방지하기 위해 여러 프로그래밍 언어들은 stack을 벗어남과 동시에 stack overflow 에러를 호출해서 프로그램을 종료시킨다.

Buffer

 데이터를 한곳에서 다른 곳으로 전송할 때 잠시 거쳐가는 메모리의 한 영역이다. 주로 사용자의 입력값을 받아들이고 입력값을 다른 곳에 사용하고자 할 때 그 입력값을 버퍼에 저장하고 추후에 버퍼에서 꺼내쓰게 된다. 이러한 과정을 버퍼링(buffering)이라고 한다.

Buffer Overflow

 버퍼에 데이터가 저장되는 과정에 서 할당되었던 크기보다 더 큰 데이터가 들어가게 되면서 버퍼 옆에 있는 공간을 침범하면서 덮어쓰게 되는 오류를 말한다. 마찬가지로 의도하지 않은 위치의 메모리를 덮어쓰기 때문에 데이터의 손실이 생길 수 있고, 이 허점을 이용해 공격자가 악의적으로 데이터를 없애버릴 수도 있다. 

 사용자가 동적할당메모리 (heap영역)를 제대로 관리해주지 않을 경우 원래 할당되었던 공간을 다른 것으로 덮어씌울 수 있게 되는데, 이 경우 buffer overflow로 이어질 수 있다. Heap 영역에서 발생한 경우, 이를 heap overflow라고도 한다.

Use After Free (UAF)

Heap 영역에서 할당된 (malloc ) 공간을 free로 영역을 해제하고, 메모리를 다시 할당 시 같은 공간을 재사용 하면서 생기는 취약점

(1) CVE-2019-14533

CVE - CVE-2019-14533

Discovered a use-after-free (UAF) affecting WMV and WMA files (ASF container
). This UAF is triggered when the video is forwarded, in other words, when the user clicks on the time bar. This bug is due to a not nulled pointer in [DemuxEnd](https://github.com/videolan/vlc/blob/5de591e391ec000f89acbaece4934110cab46e31/modules/demux/asf/asf.c#L1382)
, which later, causes a dereferencing of previously freed memory (use-after-free read). This bug could allow an attacker to alter the expected application flow.

WMV/WMA 파일에 영향을 끼치는 UAF 오류를 발견함. 이 UAF 오류는 비디오가 사용자에 의해 앞당겨질때 발생하고, 아래를 참고하면, [DemuxEnd](https://github.com/videolan/vlc/blob/5de591e391ec000f89acbaece4934110cab46e31/modules/demux/asf/asf.c#L1382) 함수에서 null pointer가 되어야 하지만 그렇지 못하여 발생한다. 이 버그는 이전에 해제된 메모리(UAF)의 역참조를 일으켜 예상되는 응용 프로그램 실행 절차를 변경하여 공격할 수 있다.

static void DemuxEnd( demux_t *p_demux )
{
    demux_sys_t *p_sys = p_demux->p_sys;

    if( p_sys->p_root )
    {
        ASF_FreeObjectRoot( p_demux->s, p_sys->p_root );
        p_sys->p_root = NULL;
	//p_sys->p_fp should also be nulled
    }

    [...]
}

VLC Vulnerabilities Discovered by the GitHub Security Research Team

(2) CVE-2019-14438

CVE - CVE-2019-14438

Description:

A heap-based buffer over-read in xiph_PackHeaders() in modules/demux/xiph.h in VideoLAN VLC media player 3.0.7.1 allows remote attackers to trigger a heap-based buffer over-read via a crafted .ogg file.

동적 할당과 관련해서 잘 조작한 .ogg 확장자 파일을 통해 buffer over-read를 유발해 buffer영역을 근처의 임의의 데이터를 읽어낼 수 있게 되는 버그이다.

문제 코드:

// vlc-3.0.7.1/modules/demux/xiph.h 에 위치한 함수
// xiph_PackHeaders()
static inline int xiph_PackHeaders(int *extra_size, void **extra,
                                   unsigned packet_size[], const void *packet[], unsigned packet_count )
{
    if (packet_count <= 0 || packet_count > XIPH_MAX_HEADER_COUNT)
        return VLC_EGENERIC;

    /* Compute the size needed for the whole extra data */
    unsigned payload_size = 0;
    unsigned header_size = 1;
    for (unsigned i = 0; i < packet_count; i++) {
        payload_size += packet_size[i];
        if (i < packet_count - 1)
            header_size += 1 + packet_size[i] / 255;
    }

    /* */
    *extra_size = header_size + payload_size;
    *extra = malloc(*extra_size);
    if (*extra == NULL)
        return VLC_ENOMEM;

    /* Write the header */
    uint8_t *current = (uint8_t*)*extra;
    *current++ = packet_count - 1;
    for (unsigned i = 0; i < packet_count - 1; i++) {
        unsigned t = packet_size[i];
        for (;;) {
            if (t >= 255) {
                *current++ = 255;
                t -= 255;
            } else {
                *current++ = t;
                break;
            }
        }
    }

    /* Copy the payloads */
    for (unsigned i = 0; i < packet_count; i++) {
        if (packet_size[i] > 0) {
            memcpy(current, packet[i], packet_size[i]);
            current += packet_size[i];
        }
    }
    assert(current == (uint8_t*)*extra + *extra_size);
    return VLC_SUCCESS;
}

VLC Vulnerabilities Discovered by the GitHub Security Research Team

 .ogg 확장자 파일을 잘 조작하면 xiph_CountHeaders()함수가 header수를 잘못 count하게 하여 문제를 유발한다고 한다. 

(3) CVE-2019-14776

Fuzzing101/Exercise 7 at main · antonio-morales/Fuzzing101

AFL++: Combining Incremental Steps of Fuzzing Research 논문에서 VLC 의 fuzz 파트를 담당한 Antonio Morales의 Github Repository에 VLC의 버그를 똑같이 재현해보는 실습이 있어 이것을 똑같이 재현해보기로 . 했지만, Docker내에서 실습을 따라하려나 ASAN을 적용한 Fuzz가 되지 않아, 적용하지 않고 Fuzz를 진행하였다.

NVD

CVE-2019-14776 : A heap-based buffer over-read exists in DemuxInit() in demux/asf/asf.c in VideoLAN VLC media player 3.0.7.1 via a crafte

변형된 WMV/ASF(Windows Media Video) 파일을 통해 Out-of-bounds Read 취약점이 발견되었음

Out-of-Bounds Read는 프로그램이 의도한 버퍼의 끝 또는 시작 전에 데이터를 읽을 때 발생하는 취약점

결과적으로 원격 공격자가 서비스 거부를 유발하거나 프로세스 메모리에서 잠재적으로 중요한 정보를 해킹 가능함

CVE-2019-14776

fuzz에 앞서 ..

AFL은 비결정론적 테스트 알고리즘을 사용하기 때문에 고정 시드값(-s123)을 설정하여 FUZZ를 진행할 예정이다.

그리고 문제가 되는 VLC3.0.7.1 버전으로 설치해 줄 예정이다.

LCOV (GCOV의 reporthtml으로 보기위한 확장 함수커버리지 툴) 도 사용할 예정이다.

docker의 AFL++에서 작업할 예정이다.

VLC 설치 전 사전 작업 및 테스트

  1. VLC fuzz를 위한 폴더 생성
cd /home
mkdir fuzzing_vlc && cd fuzzing_vlc
  1. VLC 3.0.7.1 버전 다운후 압축 해제
wget https://download.videolan.org/pub/videolan/vlc/3.0.7.1/vlc-3.0.7.1.tar.xz
tar -xvf vlc-3.0.7.1.tar.xz && cd vlc-3.0.7.1/
  1. VLC 빌드
./configure --prefix= "/home/fuzzing_vlc/vlc-3.0.7.1/install" --disable-a52 --disable-lua --disable-qt --enable-run-as-root 
make -j$(nproc)

configure를 하다보니,
configure: error: Missing libav or FFmpeg. Pass --disable-avcodec to ignore this error.

에러가 발생하여, 다음과 같은 커맨드로 에러를 해결하였다.

**wget http://www.tortall.net/projects/yasm/releases/yasm-1.3.0.tar.gz
tar -xvf yasm-1.3.0.tar.gz
cd yasm-1.3.0/
./configure && make && make install 
wget https://www.ffmpeg.org/releases/ffmpeg-3.4.5.tar.gz
tar -xvf ffmpeg-3.4.5.tar.gz
cd ffmpeg-3.4.5
./configure  --enable-shared 
make && make install**

이렇게 에러를 해결 했으나,

configure: error: package requirements (xcb >= 1.6) were not met: no package 'xcb' found consider adjusting the pkg_config_path environment variable if you installed software in a non-standard prefix. alternatively, you may set the environment variables xcb_cflags and xcb_libs to avoid the need to call pkg-config. see the pkg-config man page for more details.

에러가 발생하여 다음과 같은 커맨드로 에러를 해결하였다.

sudo apt-get install libx11-xcb-dev libxcb-glx0 libxcb-glx0-dev
sudo apt-get install libxcb-dri2-0-dev

추가로 필요한 패키지는 하단의 사이트를 참고하여 오류를 해결하여 configure를 완료하였다.

Install VLC Media Player in Ubuntu

  1. VLC 작동이 잘되는 지 테스트
./bin/vlc-static --help

정상적으로 작동이 잘 되는 모습을 확인 할 수 있다.

입력파일은 ffmpeg 표본 의 short2.wmv 및 veryshort.wmv 파일 을 활용할 생각이다

Harness 작성

vlc-staticbinary 를 직접 AFL로 퍼징하면 실행속도가 정말 느리기 때문에 VLC를 퍼징할 때, Harness를 작성해서 Fuzz를 해보려 한다.
./test/vlc-demux-run.c 에 버그가 존재하므로 이를 위한 맞춤형 harness을 제작하려고 한다.

cd test
make vlc-demux-run -j$(nproc) LDFLAGS="-fsanitize=address"
cd ..

ASF demuxing을 할 때 존재하는 버그이기 때문에, vlc_demux_process_memory 함수를 호출할 예정이다.
이 함수는 메모리에 미리 저장된 데이터 버퍼를 demux 하는 함수 이다. 또한, Partial instrumentation을 위해 함수명과 파일이름이 일치하는 것들만 접근하도록 파일을 만들어 주었다.

demux/asf/asf.c
demux/asf/asfpacket.c
demux/asf/libasf.c
vlc.c

#fun: Demux
fun: WaitKeyframe
fun: SeekPercent
fun: SeekIndex
fun: SeekPrepare
#fun: Control
fun: Packet_SetAR
fun: Packet_SetSendTime
fun: Packet_UpdateTime
fun: Packet_GetTrackInfo
fun: Packet_DoSkip
fun: Packet_Enqueue
fun: Block_Dequeue
fun: ASF_fillup_es_priorities_ex
fun: ASF_fillup_es_bitrate_priorities_ex
fun: DemuxInit
fun: FlushQueue
fun: FlushQueues
fun: DemuxEnd

fun: AsfObjectHelperHave
fun: AsfObjectHelperSkip
fun: AsfObjectHelperReadString
fun: ASF_ReadObjectCommon
fun: ASF_NextObject
fun: ASF_FreeObject_Null
fun: ASF_ReadObject_Header
fun: ASF_ReadObject_Data
fun: ASF_ReadObject_Index
fun: ASF_FreeObject_Index
fun: ASF_ReadObject_file_properties
fun: ASF_FreeObject_metadata
fun: ASF_ReadObject_metadata
fun: ASF_ReadObject_header_extension
fun: ASF_FreeObject_header_extension
fun: ASF_ReadObject_stream_properties
fun: ASF_FreeObject_stream_properties
fun: ASF_FreeObject_codec_list
fun: ASF_ReadObject_codec_list
fun: ASF_ReadObject_content_description
fun: ASF_FreeObject_content_description
fun: ASF_ReadObject_language_list
fun: ASF_FreeObject_language_list
fun: ASF_ReadObject_stream_bitrate_properties
fun: ASF_FreeObject_stream_bitrate_properties
fun: ASF_FreeObject_extended_stream_properties
fun: ASF_ReadObject_extended_stream_properties
fun: ASF_ReadObject_advanced_mutual_exclusion
fun: ASF_FreeObject_advanced_mutual_exclusion
fun: ASF_ReadObject_stream_prioritization
fun: ASF_FreeObject_stream_prioritization
fun: ASF_ReadObject_bitrate_mutual_exclusion
fun: ASF_FreeObject_bitrate_mutual_exclusion
fun: ASF_FreeObject_extended_content_description
fun: ASF_ReadObject_marker
fun: ASF_FreeObject_marker
fun: ASF_ReadObject_Raw
fun: ASF_ParentObject
fun: ASF_GetObject_Function
fun: ASF_ReadObject
fun: ASF_FreeObject
fun: ASF_ObjectDumpDebug
fun: ASF_ReadObjectRoot
fun: ASF_FreeObjectRoot
fun: ASF_CountObject
fun: ASF_FindObject

fun: DemuxSubPayload
fun: ParsePayloadExtensions
fun: DemuxPayload
fun: DemuxASFPacket

Fuzz

컴파일러로 afl-clang-fast 를 사용하여 VLC를 빌드해 볼 것이다.

CC="afl-clang-fast" CXX="afl-clang-fast++" ./configure --prefix="/home/fuzzing_vlc/vlc-3.0.7.1/install" --disable-a52 --disable-lua --disable-qt --enable-run-as-root --with-sanitizer=address

AFL_IGNORE_PROBLEMS=1 AFL_LLVM_ALLOWLIST=/home/fuzzing_vlc/Partial_instrumentation make -j$(nproc)

cd test
make vlc-demux-run -j$(nproc)
cd ..

AFL_IGNORE_PROBLEMS=1 afl-fuzz -t 100 -m none -i '/home/fuzzing_vlc/InputCorpus/' -o './afl_out' -M master -- ./test/vlc-demux-run @@

docker exec -i -t jovial_fermi /bin/bash

AFL_IGNORE_PROBLEMS=1 afl-fuzz -t 100 -m none -i '/home/fuzzing_vlc/InputCorpus/' -o './afl_out' -S slave01 -- ./test/vlc-demux-run @@

fuzz를 24시간 돌려 20개의 crash를 찾아냈다.

그 중 아래 이름의 파일을 실행시켜 볼 생각이다.

**id:000000,sig:11,src:000372,time:16170943,execs:900076,op:havoc,rep:8**

AFL_IGNORE_PROBLEMS=1 /home/fuzzing_vlc/vlc-3.0.7.1/test/vlc-demux-run /home/fuzzing_vlc/vlc-3.0.7.1/afl_out/master/crashes/id:000000,sig:11,src:000372,time:16170943,execs:900076,op:havoc,rep:8

Segmentation fault가 뜨는 것을 확인해 볼 수 있다.

왜 이런 오류가 발생하는지는 gdb란 프로그램을 활용해서 살펴보려 한다.

보통은 GDB라고 부르는 GNU 디버거는 GNU 소프트웨어 시스템을 위한 기본 디버거이다. GDB는 다양한 유닉스 기반의 시스템에서 동작하는 이식성있는 디버거로, 에이다, C, C++, 포트란 등의 여러 프로그래밍 언어를 지원함

make clean
CFLAGS="-g -O0" CXXFLAGS="-g -O0" ./configure --prefix="/home/fuzzing_vlc/vlc-3.0.7.1/install" --disable-a52 --disable-lua --disable-qt --enable-run-as-root 
make -j$(nproc)

gdb 실행을 위해 일시적으로 AFL_IGNORE_PROBLEMS=1 환경변수를 설정하고 gdb를 실행한다.

export AFL_IGNORE_PROBLEMS=1
gdb --args /home/fuzzing_vlc/vlc-3.0.7.1/test/vlc-demux-run /home/fuzzing_vlc/vlc-3.0.7.1/afl_out/master/crashes/**id:000000,sig:11,src:000372,time:16170943,execs:900076,op:havoc,rep:8**

여기서 run 을 실행하면

seg fault가 발생한 것을 확인 할 수 있다.
backtracking 을 실시하는 bt 명령어를 통해 backtrace를 하면 상세한 오류 내역이 기술된다

작업순서가 스택으로 쌓여있어 맨 아래부터가 제일 먼저 실행된 모습인 것을 확인해 볼 수 있다.

Starting program: /home/vlc-3.0.7.1/test/vlc-demux-run /home/vlc-3.0.7.1/afl_out/master/crashes/id:000002,sig:11,src:000224+000431,time:21405496,execs:1180772,op:splice,rep:32
warning: Error disabling address space randomization: Operation not permitted
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

실행 결과

Program received signal SIGSEGV, Segmentation fault.
input_vaControl (p_input=0x0, i_query=43, args=args@entry=0x7ffdd9b8e270) at input/control.c:545
545 input_resource_HoldVouts( priv->p_resource, ppp_vout, pi_vout );

(gdb) bt (함수 호출 과정 backtracking)

#0 input_vaControl (p_input=0x0, i_query=43, args=args@entry=0x7ffdd9b8e270) at input/control.c:545

#1 0x00007f576c3bebab in input_Control (p_input=0x0, i_query=-642194776, i_query@entry=43)
at input/control.c:59
#2 0x00007f576bf9f694 in input_GetVout (p_input=0x0) at ../include/vlc_input.h:568
#3 Packet_SetAR (p_packetsys=p_packetsys@entry=0x56271d6c1648, i_stream_number=,
i_ratio_x=, i_ratio_y=) at demux/asf/asf.c:571

vout_thread_t *p_vout = input_GetVout( p_demux → p_input);

#4 0x00007f576bfa877f in ParsePayloadExtensions (p_packetsys=0x0, p_packetsys@entry=0x56271d6c1648,
p_tkinfo=0x56271d6c4d98,
p_data=0x56271d6b483d "J\025d9v\030P9\226\027PA@)ll\202/\002\t.\240j!\t\232\236~7@\202\205h;\017\035V\344\271T#\241\200", i_data=, i_data@entry=40, b_keyframe=b_keyframe@entry=0x7ffdd9b8e42f,
pi_extension_pts=pi_extension_pts@entry=0x7ffdd9b8e420) at demux/asf/asfpacket.c:163
#5 0x00007f576bfa7854 in DemuxPayload (p_packetsys=0x56271d6c1648, pkt=0x7ffdd9b8e3e0,
i_payload=) at demux/asf/asfpacket.c:240
#6 DemuxASFPacket (p_packetsys=p_packetsys@entry=0x56271d6c1648, i_data_packet_min=,
i_data_packet_max=) at demux/asf/asfpacket.c:500
#7 0x00007f576bf9acf1 in Demux (p_demux=0x56271d6c37d0) at demux/asf/asf.c:220
ㄴpacket을 읽고 demux하는 과정
#8 0x000056271cf637d5 in demux_Demux (p_demux=0x56271d6c37d0) at ../include/vlc_demux.h:354
#9 demux_process_stream (args=args@entry=0x7ffdd9b8e5a8, s=) at src/input/demux-run.c:284
#10 0x000056271cf63dc5 in vlc_demux_process_url (args=0x7ffdd9b8e5a8,
url=0x56271d645510 "file:///home/vlc-3.0.7.1/afl_out/master/crashes/id%3A000002%2Csig%3A11%2Csrc%3A000224%2B000431%2Ctime%3A21405496%2Cexecs%3A1180772%2Cop%3Asplice%2Crep%3A32") at src/input/demux-run.c:326
#11 vlc_demux_process_path (args=args@entry=0x7ffdd9b8e5a8, path=)
at src/input/demux-run.c:340
#12 0x000056271cf6352b in main (argc=2, argv=0x7ffdd9b8e6e8) at vlc-demux-run.c:50


(4) 새로운 버그 발견

하다보니 클론한 vlc 취약점이 아닌 전혀 새로운게 반복해서 나왔다. 이상하다 싶어 docker 밖에서도 fuzz를 진행했지만, 몇백개의 crash들이 같은 부분에서 발생하였다. input/control.c에서 READ memory access 오류(SEGV)가 발생한다는 것이다. 이를 직접적으로 언급한 VLC의 CVE-LIST는 일단 존재하지 않았다.

SIGSEGV포인터가 가리키는 메모리가 사용하면 안되는 곳인데, 이 메모리 공간를 멋대로 사용하려고할 때 발생한다고 한다. 대표적으로 null pointer dereference. 즉, 널포인터가 가리키는 데이터를 읽거나 쓰려고 할 때이다. 이와 비슷한 버그는 CVE-2019-14534 에서 발생하였다
**(asf.c 파일에서 nullpointer dereference[역참조] 발생)** SeekPercent 함수에서 역참조가 발생한다고 하지만, 우리의 backtrace를 살펴보면 SeekPercent를 사용한 흔적이 보이지 않았다. 하지만 우리가 fuzz를 하며 참고했던 Partial instrumentation을 살펴보면 fun: SeekPercent가 존재하고, fuzz의 대상이 되는 vlc-demux-run과 전부 관련있는 것들을 모아놓은 것이기 때문에, 조금이라도 연관이 있지 않을까 라는 추측을 해보고 있다.

혹시 control.c에서 발생했으니 CVE-2019-14533 와 관련이 있지 않을까 싶었지만, 요약문 속, The Control function of demux/asf/asf.c in VideoLAN VLC media player 3.0.7.1 has a use-after-free을 보면 control function이 control.c 와 관련이 있는진 잘 모르고, CVE-2019-14533 문서를 보면 [DemuxEnd](https://github.com/videolan/vlc/blob/5de591e391ec000f89acbaece4934110cab46e31/modules/demux/asf/asf.c#L1382) 함수 에서 발생한다고 알 수 있으므로 이 추측은 거의 틀렸다고 볼 수 있다.

asf: Fix out of bound read · videolan/vlc@fdbdd67

우선 원래 찾으려 했던 취약점이 존재했는지 확인하기위해, 다음과 같은 과정을 거쳐 실험을 하였다.

  1. 도커 밖에서도 똑같이 빌드하고 fuzz 시도 해보기
  2. 해당 부분에서 이슈가 생기는 부분만 디버그해서 다시 Fuzzing 하기 ver.3.0.7
  3. 해당 논문에서 나왔던 취약점들이 전부 보완된 ver.3.0.8 버전에서 다시 Fuzzing 하기
  4. 최신버전 ver.3.0.18 에서 다시 Fuzzing 하기

1번) 해당 버그가 발생하는 부분만 패치를 적용하여 6개의 parallelizd fuzzer를 실행 중이다. 해당 버그가 발생하는 부분만 패치를 적용한 것도 1시간을 돌려보니 여전히 버그가 생성되었고, 역시나 똑같은 부분에서 오류가 발생했다.


2번) 우리가 찾은 오류는 CVE와 상관이 없을 것같아, 논문에 적힌 취약점이 전부 패치가 된 3.0.8버전으로 parallelizd fuzz를 진행하였다. 운좋게 몇 분 지나지않아 crash를 발견해서 확인해봤으나 3.0.8에도 똑같은 부분에서 오류가 발생하는 것을 확인할 수 있다.

Security Bulletin VLC 3.0.8

패치노트를 보면 논문에 있던 모든 CVE LIST에 있던 취약점들이 전부 패치가 된 것을 알 수 있다. 혹시 몰라 Docker 밖에서도 ASAN을 적용하여 실행하였지만 , 똑같은 SIGSEGV 오류가 발생했다

3번) 최신버전인 VLC-3.0.18을 동일하게 세팅해서 퍼징하였으나, 최신버전에서도 SIGSEGV가 감지되었다.

역시 이전 버전과 같은 위치에서 문제가 발생했다. control.c에서 문제가 발생해서 코드를 좀 더 분석 해보았다.

(4-1) Bug가 발생하는 VLC 코드 구조 분석

control.c안에 있는 input_resource_HoldVouts()함수에서 에러가 났다는 것은, 해당 함수를 실행하기 위해 매개변수들을 넣어주는 과정에서 매개변수 자체에서 문제가 발생했을 것이다.

VLC내에 있는 src/input/control.c 에 있는 input_vaControl 함수에 있는 input_resource_HoldVouts() 을 살펴보았다.

/src/input/resource.h 에 시그니쳐만 동일한 정의되지 않은 메소드가 있기는 하다.

해당 함수의 소스코드는 동일 디렉토리 내 resource.c에 위치해 있다.

안에 또 HoldVouts라는 함수가 있다. 찾아보기로 했다.

동일 소스코드에 있다.


만약 제대로 진행했다면, 다음과 같은 오류 메세지를 확인할 수 있었을 것이다.

Heap-buffer-overflow가 발생한다는 것을 확인할수 있다.

본래의 CVE-2019-14776demux/asf/asf.c에 있는 DemuxInit() 함수에서 heap-based buffer over read가 발생한다고 한다. 그런데 해당 문서에서는 이 문제를 찾기 위해서 /test/vlc-demux-run을 퍼징한다. 즉, 버그가 발생하는 곳과 그것을 실제로 실행시키는 binary를 찾아야 한다는 것이다.

우선 DemuxInit()을 살펴보았다. 너무 길어서 여기에 적어놓지는 못했다.

그리고 vlc-demux-run.c를 살펴보았다.

// vlc-demux-run.c

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include <stdio.h>
#include "src/input/demux-run.h"

int main(int argc, char *argv[])
{
    const char *filename;
    struct vlc_run_args args;
    vlc_run_args_init(&args);
    
    switch (argc)
    {
        case 2:
            filename = argv[argc - 1];
            break;
        default:
            fprintf(stderr, "Usage: [VLC_TARGET=demux] %s <filename>\n", argv[0]);
            return 1;
    }

    return -vlc_demux_process_path(&args, filename);
}
 생각보다 코드가 짧아서 적어놨다.

crash 결과물을 바탕으로 트레이스 해보기로 했다.

  1. struct vlc_run_args args;: 해당 구조체가 어디에 있고 어떻게 생겼는지 찾아야한다. 1주일 정도 조사해 보면서 구조체에 잘못된 자료가 들어간게 아닌가하는 의문이 지속적으로 생겼기 때문이다.

    해당 구조체는 demux-run.c에서 여러번 쓰이고 있다. (#include “src/input/demux-run.c”)

  2. return -vlc_demux_process_path(&args, filename);: vlc_demux_process_path함수는 demux-run.c에 있다. URL에는 파일이름이 들어가는 걸 알 수 있다.

int vlc_demux_process_path(const struct vlc_run_args *args, const char *path)
{
    char *url = vlc_path2uri(path, NULL);
    if (url == NULL)
    {
        fprintf(stderr, "Error: cannot convert path to URL: %s\n", path);
        return -1;
    }

    int ret = vlc_demux_process_url(args, url); // url에는 파일이름이 들어간다.
    free(url);
    return ret;
}
  1. vlc_demux_process_url(args, url): 같은 파일에 이 함수가 들어있다.
int vlc_demux_process_url(const struct vlc_run_args *args, const char *url)
{
    libvlc_instance_t *vlc = libvlc_create(args);
    if (vlc == NULL)
        return -1;

    stream_t *s = vlc_access_NewMRL(VLC_OBJECT(vlc->p_libvlc_int), url);
    if (s == NULL)
        fprintf(stderr, "Error: cannot create input stream: %s\n", url);

    int ret = demux_process_stream(args, s);  // 파일이름을 통해 stream을 추출하는 듯?emux_Demuxemux_Demux
    libvlc_release(vlc);
    return ret;
}
  1. demux_process_stream(args, s): 역시 같은 파일에 들어있다.
static int demux_process_stream(const struct vlc_run_args *args, stream_t *s)
{
    const char *name = args->name;
    if (name == NULL)
        name = "any";

    if (s == NULL)
        return -1;

    es_out_t *out = test_es_out_create(VLC_OBJECT(s));
    if (out == NULL)
        return -1;

    demux_t *demux = demux_New(VLC_OBJECT(s), name, "", s, out);
    if (demux == NULL)
    {/* Call controls for increased code coverage */
            demux_Control(demux, DEMUX_GET_SEEKPOINT, &seekpoint);
            demux_Control(demux, DEMUX_GET_POSITION, &position);
            demux_Control(demux, DEMUX_GET_TIME, &time);
            demux_Control(demux, DEMUX_GET_LENGTH, &length);
        }
        i++;
    }

    demux_Delete(demux);
    es_out_Delete(out);

    debug("Completed with %ju iteration(s).\n", i);

    return val == VLC_DEMUXER_EOF ? 0 : -1;
}
        es_out_Delete(out);
        vlc_stream_Delete(s);
        debug("Error: cannot create demultiplexer: %s\n", name);
        return -1;
    }

    uintmax_t i = 0;
    int val;

    while ((val = demux_Demux(demux)) == VLC_DEMUXER_SUCCESS)
    {
        if (args->test_demux_controls)
        {
            if (demux_test_and_clear_flags(demux, INPUT_UPDATE_TITLE_LIST))
                demux_get_title_list(demux);

            if (demux_test_and_clear_flags(demux, INPUT_UPDATE_META))
                demux_get_meta(demux);

            int seekpoint = 0;
            double position = 0.0;
            mtime_t time = 0;
            mtime_t length = 0;

            /* Call controls for increased code coverage */
            demux_Control(demux, DEMUX_GET_SEEKPOINT, &seekpoint);
            demux_Control(demux, DEMUX_GET_POSITION, &position);
            demux_Control(demux, DEMUX_GET_TIME, &time);
            demux_Control(demux, DEMUX_GET_LENGTH, &length);
        }
        i++;
    }

    demux_Delete(demux);
    es_out_Delete(out);

    debug("Completed with %ju iteration(s).\n", i);

    return val == VLC_DEMUXER_EOF ? 0 : -1;
}
 5주차 때 했던 걸 참고하자면 `demux_Demux` 함수에서 문제가 생겼다고 하니 그쪽으로 넘어간다.

while ((val = demux_Demux(demux)) == VLC_DEMUXER_SUCCESS)

  1. demux_Demux(demux_t *p_demux): /include 에 위치해 있다.
VLC_USED static inline int demux_Demux( demux_t *p_demux )
{
    if( !p_demux->pf_demux )
        return VLC_DEMUXER_SUCCESS;

    return p_demux->pf_demux( p_demux );
}
 헤더 파일인데도 기능들이 정의되어 있다는 게 특이하다. 문제는 다음에 바로 `demux/asf/asf.c` 에 있는 `Demux`함수로 넘어가게 된다는데 이게 어떻게 바로 넘어갈 수 있는지 의문점이 생긴다.있는 `Demux`함수로 넘어가게 된다는데 이게 어떻게 바로 넘어갈 수 있는지 잘 모르겠다는 것이다.

 다만 알 수 있는건, `Demux`함수도 매개변수로 `demux_t *p_demux`를 받는다는 것이다. 이걸 매개로 연결이 되는 것이 아닐까라는 생각이 든다. 또한 원래대로라면 `DemuxInit`함수에서 문제가 생기는데, 이 함수도 `demux_t *p_demux`를 매개변수로 받기 때문에 연결고리는 있다고 생각한다. 

 그렇다면 `demux_t`는 무엇일까? 이 구조체가 중요한 정보를 담고 있을 것 같다는 생각이 들어 찾아보았다. 적어도 `asf.c`내에 있는 모든 함수들이 해당 구조체를 사용하기 때문이다.
  1. demux_t: /include/vlc_demux.h 에 있다.
struct demux_t
{
    VLC_COMMON_MEMBERS

    /* Module properties */
    module_t    *p_module;

    /* eg informative but needed (we can have access+demux) */
    char        *psz_access;
    char        *psz_demux;
    char        *psz_location;
    char        *psz_file;

    union {
        /**
         * Input stream
         *
         * Depending on the module capability:
       resource.c     * - "demux": input byte stream (not NULL)
         * - "access_demux": a NULL pointer
         * - "demux_filter": undefined
         */
        stream_t *s;
        /**
         * Input demuxer
         *
         * If the module capability is "demux_filter", this is the upstream
         * demuxer or demux filter. Otherwise, this is undefined.
         */
        demux_t *p_next;
    };

    /* es output */
    es_out_t    *out;   /* our p_es_out */

    bool         b_preparsing; /* True if the demux is used to preparse */
    
    /* set by demuxer */
    int (*pf_demux)  ( demux_t * );   /* demux one frame only */
    int (*pf_control)( demux_t *, int i_query, va_list args);

    /* Demux has to maintain them upto
     * when it is responsible of seekpoint/title */
    struct
    {
        unsigned int i_update;  /* Demux sets them on change,
                                   Input removes them once take into account*/
        /* Seekpoint/Title at demux level */
        int          i_title;       /* idem, start from 0 (could be menu) */
        int          i_seekpoint;   /* idem, start from 0 */
    } info;
    demux_sys_t *p_sys;

    /* Weak link to parent input */
    input_thread_t *p_input;
};
 아쉽지만 유의미한 결과를 찾지는 못했다. 

 우리가 찾은 것은 `control.c`의 545번 줄에서 `SEGV`가 나왔다는 것이다. 545번 줄은 다음과 같다.
input_resource_HoldVouts( priv->p_resource, ppp_vout, pi_vout );
 함수로 넘어가지 않고 해당 단계에서 문제가 생긴걸로 봐서는 `priv->p_resource` 에서 문제가 생겼다고 생각할 수 있다. 다른 두 인자는 함수에 넣은 거기 때문에 추가로 연산이 진행되는 것은 이거밖에 없기 때문이다. 
input_thread_private_t *priv = input_priv(p_input);
 `priv`는 포인터로 `input_thread_private_t` 구조체로 보인다. 

input_thread_private_tinput_internal.h에 있다.

/** Private input fields */
typedef struct input_thread_private_t
{
    struct input_thread_t input;

    /* Global properties */
    bool        b_preparsing;
    bool        b_can_pause;
    bool        b_can_rate_control;
    bool        b_can_pace_control;

    /* Current state */
    int         i_state;
    bool        is_running;
    bool        is_stopped;
    bool        b_recording;
    int         i_rate;

    /* Playtime configuration and state */
    int64_t     i_start;    /* :start-time,0 by default */
    int64_t     i_stop;     /* :stop-time, 0 if none */
    int64_t     i_time;     /* Current time */
    bool        b_fast_seek;/* :input-fast-seek */

    /* Output */`
    bool            b_out_pace_control; /* XXX Move it ot es_sout ? */
    sout_instance_t *p_sout;            /* Idem ? */
    es_out_t        *p_es_out;
    es_out_t        *p_es_out_display;
    vlc_viewpoint_t viewpoint;
    bool            viewpoint_changed;
    vlc_renderer_item_t *p_renderer;

    /* Title infos FIXME multi-input (not easy) ? */
    int          i_title;
    const input_title_t **title;

    int i_title_offset;
    int i_seekpoint_offset;

    /* User bookmarks FIXME won't be easy with multiples input */
    seekpoint_t bookmark;
    int         i_bookmark;
    seekpoint_t **pp_bookmark;

    /* Input attachment */
    int i_attachment;
    input_attachment_t **attachment;
    const demux_t **attachment_demux;

    /* Main input properties */

    /* Input item */
    input_item_t   *p_item;

    /* Main source */
    input_source_t *master;
    /* Slave sources (subs, and others) */
    int            i_slave;
    input_source_t **slave;

    /* Resources */
    input_resource_t *p_resource;
    input_resource_t *p_resource_private;

    /* Stats counters */
    struct {
        counter_t *p_read_packets;
        counter_t *p_read_bytes;
        counter_t *p_input_bitrate;
        counter_t *p_demux_read;
        counter_t *p_demux_bitrate;
        counter_t *p_demux_corrupted;
        counter_t *p_demux_discontinuity;
        counter_t *p_decoded_audio;
        counter_t *p_decoded_video;
        counter_t *p_decoded_sub;
        counter_t *p_sout_sent_packets;
        counter_t *p_sout_sent_bytes;
        counter_t *p_sout_send_bitrate;
        counter_t *p_played_abuffers;
        counter_t *p_lost_abuffers;
        counter_t *p_displayed_pictures;
        counter_t *p_lost_pictures;
        vlc_mutex_t counters_lock;
    } counters;

    /* Buffer of pending actions */
    vlc_mutex_t lock_control;
    vlc_cond_t  wait_control;
    int i_control;
    input_control_t control[INPUT_CONTROL_FIFO_SIZE];

    vlc_thread_t thread;
    vlc_interrupt_t interrupt;
} input_thread_private_t;
위 코드에서 문제되는 부분:
/* Resources */
    input_resource_t *p_resource;
    input_resource_t *p_resource_private;

input_thread_private_t 구조체의 *p_resource에 접근하는 도중 잘못된 위치의 메모리를 읽어내려는 시도가 있는 것 같다.

struct input_resource_t
{
atomic_uint    refs;
v1lc_object_t   *p_parent;

/* This lock is used to serialize request and protect
 * our variables */
vlc_mutex_t    lock;

/* */
input_thread_t *p_input;

sout_instance_t *p_sout;
vout_thread_t   *p_vout_free;

/* This lock is used to protect vout resources access (for hold)
 * It is a special case because of embed video (possible deadlock
 * between vout window request and vout holds in some(qt) interface)
 */
vlc_mutex_t    lock_hold;

/* You need lock+lock_hold to write to the following variables and
 * only lock or lock_hold to read them */

vout_thread_t   **pp_vout;
int             i_vout;

bool            b_aout_busy;
audio_output_t *p_aout;
};
💡 **SIGSEGV**는 시용해서는 안되는 메모리 공간을 포인터가 가리켜서 사용하게 되면 발생하는 에러이다. 대표적으로 **null pointer dereference:** 즉, 널포인터가 가리키는 데이터를 읽거나 쓰려고 할 때 발생한다.
 `priv` 포인터 변수가 null 포인터인지 확인하기 위해 `control.c` 의 문제되는 부분 바로 위에 간단히 null임을 체크하는 if문을 추가했다.

그리고 fuzzing 히면서 crash된 input을 넣어서 돌려봤다.

 if문이 실행되었고, `priv`포인터는 널포인터가 되는 것으로 드러났다.

우리가 찾던 오류는 priv 포인터가 null 값이 되는 것과 관련이 있을 것이다.

(4-2) INPUT (WMV) Analyze

또한, fuzz할 때 인풋으로 넣어주던 wmv파일의 분석도 필요하다고 생각이 들었다.
공식적으로 적힌 문서의 취약점 Description은 다음과 같았다.

변형된 WMV/ASF(Windows Media Video) 파일을 통해 Out-of-bounds Read 취약점이 발견되었음

Out-of-Bounds Read는 프로그램이 의도한 버퍼의 끝 또는 시작 전에 데이터를 읽을 때 발생하는 취약점
결과적으로 원격 공격자가 서비스 거부를 유발하거나 프로세스 메모리에서 잠재적으로 중요한 정보를 해킹 가능함

우리가 Fuzz를 할 때, 변형된 WMV파일을 input으로 주었지만 이 변형된 input 파일이 어떻게 변형된 것인지 구체적인 조사가 필요해 보인다.

우선 구체적인 조사에 앞서 새로운 입력파일은 ffmpeg 표본 에서 몇 가지 정상적인 파일을 추려 시도할 예정이다. 여러 파일들을 살펴보고나서 몇 개의 파일을 골라 testcase의 input의 사이즈가 적절히 작게 편집하였다.(1KB내외) 그리고 asf파일로도 Out-of-bounds Read가 발생하므로 역시 fuzz의 input 대상으로 삽입할 예정이다.

정상적인 파일이라면 Crash가 발생해서는 안된다,

Index of /asf-wmv

/asf-wmv/ 디렉토리의 정상적인 파일 두 개를 채택하였다.

[welcome3.wmv](https://samples.ffmpeg.org/asf-wmv/welcome3.wmv)

[elephant.asf](https://samples.ffmpeg.org/asf-wmv/elephant.asf)

이 파일들의 크기를 줄여야 하므로 Adobe Premere Pro로 파일을 몇 프레임단위로 잘라주는 과정이 필요하다

2~3프레임 정도로 wmv파일의 재생시간을 줄였지만 용량은 57kb까지밖에 낮아진다. AFL-FUZZ를 실행할 때, 인풋의 사이즈는 1kb 내외가 되어야하므로 적절하진 않다. 하지만, 거의 2시간동안 이 용량을 1kb내외로 줄일 수 있는 방법을 찾아봤으나 해결책을 찾지 못하여, 결국 이 파일을 가지고 fuzz를 진행하려 한다

https://github.com/bakukun/JARAMOPENAPI/blob/main/fuzz wmv after.wmv

위의 welcome3.wmv 파일을 줄인 파일의 링크이다.

cd /home
mkdir afterfuzzinput && cd afterfuzzinput
wget https://github.com/bakukun/JARAMOPENAPI/blob/main/fuzz%20wmv%20after.wmv
AFL_IGNORE_PROBLEMS=1 afl-fuzz -t 1000 -m none -i '/home/afterfuzzinput/' -o './afl_out' -M master -- ./test/vlc-demux-run @@

docker exec -i -t jovial_fermi /bin/bash

AFL_IGNORE_PROBLEMS=1 afl-fuzz -t 1000 -m none -i '/home/afterfuzzinput/' -o './afl_out' -S slave01 -- ./test/vlc-demux-run @@

input이 57kb으로 커서 -t 를 평소 100이 아닌 1000으로 두어야 Fuzz가 실행된다

4%86%E1%85%B5%E1%86%BE%20ba685025271045e28380ca505b9f87ab/Untitled%2099.png)

18시간이 지난 결과 당연하게도 crash가 0개 발견되었다.

파일이 어떻게 변형되었는지 슬랙 커뮤니티에 컨택을 하여 깃허브 메인테이너한테 질문을 했으나 답장이 없었다.

(4-3) Bug report

위에서 분석한 것을 토대로 VLC gitlab 커뮤니티에 issue를 열었다.
이슈가 close 되고나서, 개발자가 private하게 view 하도록 옵션을 변경하여 캡쳐본으로 첨부하였다.

VLC-3.0.18 /src/input/control.c Giving SEGV error (#27838) · Issues · VideoLAN / VLC · GitLab

버그 이슈를 올리고 VLC의 개발자들이 코멘트를 달아주었다.
https://code.videolan.org/videolan/vlc/-/issues/27838

아래는 캡처본 전문이다.


요약

VLC의 개발자의 말에 의하면, 우리가 리포트한 버그가 3.0.x 번대 빌드버전에서 재생산 되지 않는다고 한다. 그것 보다도 더 중요한 사실이 있었는데, 우리가 fuzz를 할 때 사용했던 demux와 input에 관련한 것들을 더 이상 4.0 버전에서 사용하지 않는다는 것이 가장 크다.
VLC 4.0에서 수정됨: demux가 input_thread에 더 이상 액세스하지 않음
그리고 보안적인 이슈도 아닌 것처럼 보인다 해서 현재 이슈가 Close 된 상태로 끝났다.


4. 결론 및 소감

(1) 결론 및 요약

6주라는 짧은 기간동안 연구과제를 수행하였는데, 짧게 요약을 하자면 다음과 같다.
AFL과 AFL++의 세부기능 및 특징을 자세히 살펴보았고, Docker 역시 활용해볼 수 있었다. 또한, 그것들을 기반으로 특정 소프트웨어(LEVELDB,XPDF,TCPDUMP,LibTIFF,VLC) 들을 Fuzz해가며 버그를 똑같이 구현해보았다. 그 덕분에 이 연구 주제인 확장 가능한 parallelized fuzzing 인프라를 구축 할 수 있었고 AFL++의 persistent mode를 통해 fuzz의 속도가 얼마나 차이가 나는지 성능 비교 및 분석도 가능했다.
별개로 VLC에서 새로운 버그를 찾았으나, 아쉽게도 다음 버전에서 쓰지 않는 기능이라 patch는 이루어지지 못했다는 아쉬움이 있다.


(2) 소감

Fuzz라는 것을 처음 접하였지만, 이런 소프트웨어 테스트 기법을 통해서 생각보다 많은 취약점을 찾을 수 있다는 것이 인상깊었다.

또한 fuzz를 하며 버그들을 구현해보니, 소프트웨어 개발을 진행하며 생각해 볼만한 것 또한 많아졌다. 예를 들어, 항상 입력을 받는 코드를 작성할 때 예상치 못한 모든 경우에 대해 예외처리를 해주어야 겠다는 생각도 하게 되었다. 또한 이번 논문 속 VLC의 CVE-LIST를 보면 memory와 관련한 bug-type이 많았기 때문에, UAF나 buffer overflow가 발생하지 않도록 메모리 최적화를 어떻게 할 지 고려하게 될 것 같다.


profile
천천히,꾸준하게

0개의 댓글