보안/Fuzzing101

[Fuzzing101] exercise 3 CVE-2017-13011 실습 기록

pumisj 2026. 3. 19. 23:27

서론

일단 이번 실습에서는 목표였던 CVE-2017-13028를 찾지 못했기에, 대신 CVE-2017-13011에 대해서 다루겠다.

tcpdump는 Asan으로 디버깅 하는것부터 끝까지 많이 고생했던거같다.

 

저번과 마찬가지로 실습을 진행하면서 했던 삽질을 모두 적어보려고 한다.

 

그럼 시작하겠다.


테스트 환경

작년에 마련한 T9 미니PC를 서버로 사용중이다

환경은 Ubuntu 서버 OS에 afl++ 공식 도커 이미지를 이용하여 실습을 진행할 예정이다.


CVE-2017-13011

해당 취약점은 NIC를 거치는 패킷들의 헤더를 출력해주는 tcpdump 프로그램에서 발생했다.

util-print.c 파일의 bittok2str_internal() 함수에서 해당 취약점이 발견되었으며, OOB의 한 종류인 global-buffer-overflow를 일으킨다.

 

설명에는 4.9.2 이하 버전에서 발생하는 취약점이라고 나와있지만, 4.9.2 버전에서는 4시간정도 돌려도 아무것도 발견하지 못해 결국 4.9.1버전에서 진행하였다.

 

그러면 바로 실습을 진행하도록 하겠다.


퍼징

먼저 wget으로 tcpdump-4.9.1 버전을 다운로드 받고, 여기에 사용되는 libpcap 라이브러리도 같이 다운로드한다.

 

wget https://github.com/the-tcpdump-group/tcpdump/archive/refs/tags/tcpdump-4.9.1.tar.gz
tar -xzvf tcpdump-4.9.1.tar.gz

wget https://github.com/the-tcpdump-group/libpcap/archive/refs/tags/libpcap-1.8.0.tar.gz
tar -xzvf libpcap-1.8.0.tar.gz

mv libpcap-libpcap-1.8.0/ libpcap-1.8.0

 

여기서 다운로드받은 libpcap-libpcap-1.8.0 디렉토리명을 그대로 쓰면 tcpdump에서 빌드할 때 링커가 이 폴더를 찾지 못한다.

따라서 mv로 디렉토리명을 변경해준 뒤 빌드 후 tcpdump의 tests 디렉토리에 있는 테스트파일로 실행한다. 

 

실행했을 때 정상적으로 출력된다.

이번에는 Asan (Address Sanitizer) 모드로 빌드를 했는데, Asan은 구글이 개발한 도구로 잘못된 메모리 액세스 등 메모리와 관련된 오류를 자세히 탐지한다.

 

cd $HOME/fuzzing_tcpdump/libpcap-1.8.0/
export LLVM_CONFIG="llvm-config-11"
CC=afl-clang-lto ./configure --enable-shared=no --prefix="$HOME/fuzzing_tcpdump/install/"
AFL_USE_ASAN=1 make

cd $HOME/fuzzing_tcpdump/tcpdump-tcpdump-4.9.2/
CC=afl-clang-lto CFLAGS="-Wno-error=implicit-function-declaration -Wno-error=implicit-int" ./configure --prefix="$HOME/fuzzing_tcpdump/install/"
AFL_USE_ASAN=1 make
AFL_USE_ASAN=1 make install

 

이번에도 컴파일러는 afl-clang-lto를 사용했으며, tcpdump의 경우에는 오래된 코드이다보니 변수, 함수 타입을 생략해서 자동으로 int가 됐을 때 오류가 발생하여 Makefile을 만드는데 자꾸 실패했다.

이에 대한 해결 방법으로 configure을 실행할 때 위와 같이 오류 대신 경고만 주는 flag를 설정해야한다.

 

afl-fuzz -i -m none <인풋 디렉토리> -o <아웃풋 디렉토리> -s 123 -- <실행 파일> -vvvvXX -ee -nn -r @@

 

 

Asan을 사용하면 64bit 시스템에서 많은 가상메모리 공간을 필요로한다. 그래서 -m none을 적어 퍼징할 때 메모리 제한을 해제한다. 

 

 

위 사진은 퍼징 중간에 캡쳐한거고, 결국  CVE-2017-13028와 관련된 crash 파일을 찾지 못해 현재까지 약 3일동안 퍼징을 돌리고있다.

오류 발견

=================================================================
==3752954==ERROR: AddressSanitizer: global-buffer-overflow on address 0x5d04c889fb24 at pc 0x5d04c752fdc2 bp 0x7ffed2889c00 sp 0x7ffed28893a0
WRITE of size 17 at 0x5d04c889fb24 thread T0
    #0 0x5d04c752fdc1 in vsnprintf (/home/fuzzing_tcpdump/install/sbin/tcpdump+0x284dc1) (BuildId: 76aca6779ce4a98b9db83df42ae3ce243196ec7d)
    #1 0x5d04c75316c1 in snprintf (/home/fuzzing_tcpdump/install/sbin/tcpdump+0x2866c1) (BuildId: 76aca6779ce4a98b9db83df42ae3ce243196ec7d)
    #2 0x5d04c78d2e22 in bittok2str_internal /home/fuzzing_tcpdump/tcpdump-tcpdump-4.9.1/./util-print.c:540:29
    #3 0x5d04c777b925 in bittok2str /home/fuzzing_tcpdump/tcpdump-tcpdump-4.9.1/./util-print.c:575:13
    #4 0x5d04c777b925 in lldp_private_8023_print /home/fuzzing_tcpdump/tcpdump-tcpdump-4.9.1/./print-lldp.c:872:9
    #5 0x5d04c777b925 in lldp_print /home/fuzzing_tcpdump/tcpdump-tcpdump-4.9.1/./print-lldp.c:1616:31
    #6 0x5d04c76bdc80 in ethertype_print /home/fuzzing_tcpdump/tcpdump-tcpdump-4.9.1/./print-ether.c:408:3
    #7 0x5d04c76bd1d7 in ether_print /home/fuzzing_tcpdump/tcpdump-tcpdump-4.9.1/./print-ether.c:236:7
    #8 0x5d04c7603e1f in pretty_print_packet /home/fuzzing_tcpdump/tcpdump-tcpdump-4.9.1/./print.c:339:18
    #9 0x5d04c7603e1f in print_packet /home/fuzzing_tcpdump/tcpdump-tcpdump-4.9.1/./tcpdump.c:2506:2
    #10 0x5d04c7a117bc in pcap_offline_read /home/pumisj/fuzzing-study/fuzzing_tcpdump/libpcap-1.8.0/./savefile.c:507:4
    #11 0x5d04c75fab18 in pcap_loop /home/pumisj/fuzzing-study/fuzzing_tcpdump/libpcap-1.8.0/./pcap.c:875:8
    #12 0x5d04c75fab18 in main /home/fuzzing_tcpdump/tcpdump-tcpdump-4.9.1/./tcpdump.c:2009:12
    #13 0x757a6c5141c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 8e9fd827446c24067541ac5390e6f527fb5947bb)
    #14 0x757a6c51428a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 8e9fd827446c24067541ac5390e6f527fb5947bb)
    #15 0x5d04c7509254 in _start (/home/fuzzing_tcpdump/install/sbin/tcpdump+0x25e254) (BuildId: 76aca6779ce4a98b9db83df42ae3ce243196ec7d)

0x5d04c889fb24 is located 4 bytes after global variable 'bittok2str_internal.buf' defined in '/home/fuzzing_tcpdump/tcpdump-tcpdump-4.9.1/./util-print.c:524' (0x5d04c889fa20) of size 256
  'bittok2str_internal.buf' is ascii string '10BASE-T hdx, 10BASE-T fdx, 100BASE-T4, 100BASE-TX hdx, 100BASE-TX fdx, 100BASE-T2 hdx, 100BASE-T2 fdx, Pause for fdx links, Asym PAUSE for fdx, Sym PAUSE for fdx, Asym and Sym PAUSE for fdx, 1000BASE-{X LX SX CX} hdx, 1000BASE-{X LX SX CX} fdx, 1000BASE-'
SUMMARY: AddressSanitizer: global-buffer-overflow (/home/fuzzing_tcpdump/install/sbin/tcpdump+0x284dc1) (BuildId: 76aca6779ce4a98b9db83df42ae3ce243196ec7d) in vsnprintf
Shadow bytes around the buggy address:
  0x5d04c889f880: 00 00 00 00 00 00 00 00 00 00 00 00 f9 f9 f9 f9
  0x5d04c889f900: f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 04 f9 f9 f9
  0x5d04c889f980: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x5d04c889fa00: f9 f9 f9 f9 00 00 00 00 00 00 00 00 00 00 00 00
  0x5d04c889fa80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x5d04c889fb00: 00 00 00 00[f9]f9 f9 f9 f9 f9 f9 f9 00 00 00 00
  0x5d04c889fb80: 04 f9 f9 f9 04 f9 f9 f9 04 f9 f9 f9 00 00 00 00
  0x5d04c889fc00: 04 f9 f9 f9 00 00 00 00 00 00 00 00 00 00 00 00
  0x5d04c889fc80: 00 00 00 00 f9 f9 f9 f9 00 00 00 00 00 00 00 00
  0x5d04c889fd00: 00 00 00 00 00 00 00 00 f9 f9 f9 f9 04 f9 f9 f9
  0x5d04c889fd80: 00 f9 f9 f9 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca

 

Asan을 이용하면 GDB를 사용하지 않아도 어느부분에서 오류가 발생했는지 함수 스택과 Shadow Byte map을 보여준다.

 

해당 오류는 BSS또는 Data segment 구역의 전역변수에 잘못접근해 발생한 global-buffer-overflow로 Shadow byte map을 보면 알 수 있다시피 Global redzone인 f9 구역을 건들여 발생한 오류이다.

 

위에 함수 콜 스택이 있어 그대로 코드를 보고 문제 지점을 찾아도 되지만, 정확한 메모리 값을 확인하고싶기에 GDB로 디버깅을 진행하였다.


디버깅

Asan을 사용하면 메모리에 Redzone을 지정해 잘못된 메모리 액세스를 발견하면 바로 프로그램을 종료하는데에 반해, 보통은 그런거 없이 메모리에 잘못접근해도 그냥 그 상태로 진행되기에 Asan없이 그냥 gcc로 컴파일하면 오류가 발생하지 않는 경우도 있다.

같은 crash를 유발하는 파일인데 Asan 없이 실행할 경우 정상적으로 종료됨.

 

따라서 gcc로 컴파일할때도 Asan을 사용하겠다고 명시해야한다.

export ASAN_OPTIONS=abort_on_error=1
CC="gcc" CFLAGS="-fsanitize=address -g -O0" ./configure --prefix="/home/fuzzing_tcpdump/gcc_install"

 

Asan은 오류 발견시 콜 스택을 모두 초기화하고 프로그램을 종료하기때문에 에러가 발생하더라도 종료되지 않게 옵션을 넣었다.

 

다음으로 GDB을 이용해 디버깅을 시작했다.

 

 

 #0~8번은 sanitizer가 vsnprintf를 대신 실행하고, 오류가 발생해 프로그램을 종료하는 상황이므로, 9번부터 코드를 살펴보면 된다.

bittok2str_internal 함수 내부

 bittok2str_internal함수는 입력 파일에서 가져온 값을 따라 통신 규격이나 장비 등 string으로 바꿔 buf에 담아주는 함수이다.

 여기서는 snprintf를 사용해 buf+buflen 위치에 sizeof(buf)-buflen만큼의 공간에 문자열을 출력한 뒤 buflen에 sizeof(buf)-buflen만큼의 값을 더해준다.

 

해당 부분을 print로 값을 찍어보았다.

그 중 sizeof(buf)-buflen의 값이 0xFFFFFFFC로 0x100 - 0x104에서 overflow가 일어난걸 확인할 수 있었다.

왜 이런 값이 들어왔는지 코드를 보면서 추적해보았다.

 

static char *
bittok2str_internal(register const struct tok *lp, register const char *fmt,
	   register u_int v, const char *sep)
{
        static char buf[256]; /* our stringbuffer */
        int buflen=0;
        register u_int rotbit; /* this is the bit we rotate through all bitpositions */
        register u_int tokval;
        const char * sepstr = "";

	while (lp != NULL && lp->s != NULL) {
            tokval=lp->v;   /* load our first value */
            rotbit=1;
            while (rotbit != 0) {
                /*
                 * lets AND the rotating bit with our token value
                 * and see if we have got a match
                 */
		if (tokval == (v&rotbit)) {
                    /* ok we have found something */
                    buflen+=snprintf(buf+buflen, sizeof(buf)-buflen, "%s%s",
                                     sepstr, lp->s);
                    sepstr = sep;
                    break;
                }
                rotbit=rotbit<<1; /* no match - lets shift and try again */
            }
            lp++;
	}

        if (buflen == 0)
            /* bummer - lets print the "unknown" message as advised in the fmt string if we got one */
            (void)snprintf(buf, sizeof(buf), fmt == NULL ? "#%08x" : fmt, v);
        return (buf);
}

 

 rotbit은 0b00000001로, 왼쪽으로 1씩 비트시프트를 진행하니 내부 while문은 총 8번 반복한다. 또 buflen을 증가시키려면 tokval == (v&rotbit)을 만족해야하는데, v의 값이 0xFFFF라면 항상 마스킹이 되기때문에 tokval과 일치할경우 항상 buflen이 증가한다.

 

따라서 static으로 선언되어있는 buf에 점점 문자가 입력되면서 결국 최대 범위인 256에서 추가적인 문자열이 더해져 overflow가 발생한것이다.

 

그러면 문제는 0xFFFF로 들어오는 v의 값일것이다. 이를 확인하기 위해서 11번 프레임을 확인해봤다.

 

bittok2str을 실행하는 인자값으로 EXTRACT_16BITS(tptr+5)가 있다. 입력 파일에서 이 부분에 해당하는 HEX값을 보면 다음과 같다.

 

tptr+5가 가리키는 데이터

00000800C부분부터 16비트, 즉 2바이트 0xFFFF가 v값이 된것이다.

실제로 이 FF FF부분을 낮은 값으로 바꾸면 정상적으로 실행이 된다.

 

따라서 이를 처리하는 부분을 패치하면 된다.


패치

처음에 나는 단순히 buf값을 늘리면 된다고 생각했다. 그래서 buf의 크기를 256에서 1024로 늘렸고, 실제로 내가 찾은 crash파일에 대해서는 모두 문제가 해결되었다.

 

하지만 CVE-2017-13011 공식 패치는 다음과 같이 수정되었다.

https://github.com/the-tcpdump-group/tcpdump/commit/9f0730bee3eb65d07b49fd468bc2f269173352fe

 

 일단 나와 마찬가지로 buf의 크기를 키웠는데, 버퍼의 주소인 bufp, 버퍼의 남은 공간인 space_left, 저장될 문자열의 크기인 string_size를 새로 선언하였다.

 

https://github.com/the-tcpdump-group/tcpdump/commit/9f0730bee3eb65d07b49fd468bc2f269173352fe

그 이후 문제가 되던 snprintf를 삭제하고, 대신 남은 공간을 계속 체크하며 문자를 출력하는걸 확인할 수 있다.

 

FFFF라면 버퍼크기 1024정도면 커버할 수 있을거라 생각했는데, 문제는 이 패킷 전송 프로토콜이 LLDP라는거다.

LLDP는 인접한 통신장비끼리 서로 정보를 공유하는 프로토콜이라고 한다. 이런 LLDP는 TLV를 하나만 담을 수 있는게 아니라 여러개를 담을 수 있는데, 악의적인 공격으로 TLV 10개를 넣는다면 버퍼 크기가 1024라도 언제든지 터질 수 있는거다.

 

그렇기에 공식에서는 계속 버퍼에 남는 공간을 계산해서 처리하는 로직을 작성한거라고 생각이든다.

패치 적용 후 정상작동 확인

 


후기

저번에 실습한 exercise2 패치는 정말 단순히 오류만 안뜨면 된다는 생각으로 검증로직 하나만 추가하고 통과하지 못하면 프로그램을 종료하는 방식으로 수정했었다.

 

하지만 이번 공식 패치내역을 보니 프로그램을 종료시키는게 아니라 메모리 검증만 정밀하게 차단하고 서비스는 그대로 유지하는 방식의 패치가 서비스 안정성 측면에서, 그리고 DoS를 막는 효과적인 방법임을 깨달았다.

 

사실 내가 찾은 PoC중에는 CVE-2017-12900를 트리거하는 케이스가 포함되어있었다. 해당 취약점은 lspping_print 함수가 호출하는 tok2strbuf에서 발생한 global-buffer-overflow 취약점인데, 나중에 기회가 있다면 근본 원리를 찾아서 디버깅해보면 흥미로울거같다.

 

'보안 > Fuzzing101' 카테고리의 다른 글

[Fuzzing101] exercise 2 CVE-2012-2836 실습 기록  (0) 2026.03.15