기본 콘텐츠로 건너뛰기

CVE-2014-0160_LIBFUZZER

CVE-2014-0160(HeartBleed)_LIBFUZZER

벌써 발견된지 3년이나 지난 취약점이 된 HeartBleed를 이용해서 LIBFUZZER 사용법을 숙지해보자. 구글에서는 친절하게 LIBFUZZER 를 학습할 수 있게 Testcase들을 제공해주고 있다.

target.cc

Openssl 퍼징 테스트를 위해서 작성된 target.cc파일을 먼저 살펴보자.

#include <openssl/ssl.h>
#include <openssl/err.h>
#include <assert.h>
#include <stdint.h>
#include <stddef.h>

#ifndef CERT_PATH
# define CERT_PATH
#endif

SSL_CTX *Init() {
  SSL_library_init();
  SSL_load_error_strings();
  ERR_load_BIO_strings();
  OpenSSL_add_all_algorithms();
  SSL_CTX *sctx;
  assert (sctx = SSL_CTX_new(TLSv1_method()));
  /* These two file were created with this command:
      openssl req -x509 -newkey rsa:512 -keyout server.key \
     -out server.pem -days 9999 -nodes -subj /CN=a/
  */
  assert(SSL_CTX_use_certificate_file(sctx, CERT_PATH "server.pem",
                                      SSL_FILETYPE_PEM));
  assert(SSL_CTX_use_PrivateKey_file(sctx, CERT_PATH "server.key",
                                     SSL_FILETYPE_PEM));
  return sctx;
}
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
  static SSL_CTX *sctx = Init();
  SSL *server = SSL_new(sctx);
  BIO *sinbio = BIO_new(BIO_s_mem());
  BIO *soutbio = BIO_new(BIO_s_mem());
  SSL_set_bio(server, sinbio, soutbio);
  SSL_set_accept_state(server);
  BIO_write(sinbio, Data, Size);
  SSL_do_handshake(server);
  SSL_free(server);
  return 0;
}

Openssl 사용을 위한 간단한 코드를 확인할 수 있는 데, LLVMFuzzerTestOneInput 인자 값인 Data, SizeBIO_write함수의 인자 값으로 지정한 것을 볼 수 있다.

BIO_write(out, buf, len)
buf에서 len만큼 out에 write한다.

Created with Raphaël 2.1.2StartCreate a New SSLCreate a New memory BIO (rbio, wbio)Connects the BIOs rbio and wbioSets ssl to work in server modeWrite fuzzing dataTLS/SSL handshakeFree Allocate SSL structEnd

아래 명령어를 수행하면 openssl-1.0.1f 파일이 생성된 것을 확인할 수 있다. 파일을 실행하면 몇 초 뒤에 결과를 확인할 수 있다.

mkdir -p ~/heartbleed; rm -rf ~/heartbleed/*; cd ~/heartbleed
~/FTS/openssl-1.0.1f/build.sh

openssl-1.0.1f

Analysis

=================================================================
==2674:2674==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x629000009748 at pc >0x0000004bcfe5 bp 0x7ffee4c21520 sp 0x7ffee4c20cd0
READ of size 23040 at 0x629000009748 thread T0
#0 0x4bcfe4 in __asan_memcpy (/root/heartbleed/openssl-1.0.1f+0x4bcfe4)
#1 0x4f8232 in tls1_process_heartbeat /root/heartbleed/BUILD/ssl/t1_lib.c:2586:3
#2 0x568cd2 in ssl3_read_bytes /root/heartbleed/BUILD/ssl/s3_pkt.c:1092:4
#3 0x56d4b1 in ssl3_get_message /root/heartbleed/BUILD/ssl/s3_both.c:457:7
#4 0x536599 in ssl3_get_client_hello /root/heartbleed/BUILD/ssl/s3_srvr.c:941:4
#5 0x532642 in ssl3_accept /root/heartbleed/BUILD/ssl/s3_srvr.c:357:9
#6 0x4ebacc in LLVMFuzzerTestOneInput /root/FTS/openssl-1.0.1f/target.cc:38:3
#7 0x8203c3 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) /root/Fuzzer/>FuzzerLoop.cpp:493:13
#8 0x8205f0 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long) /root/Fuzzer/>FuzzerLoop.cpp:450:3
#9 0x82192b in fuzzer::Fuzzer::MutateAndTestOne() /root/Fuzzer/FuzzerLoop.cpp:700:30
#10 0x821b87 in fuzzer::Fuzzer::Loop() /root/Fuzzer/FuzzerLoop.cpp:732:5
#11 0x8191e4 in fuzzer::FuzzerDriver(int*, char***, int ()(unsigned char const, unsigned long)) />root/Fuzzer/FuzzerDriver.cpp:567:6
#12 0x816cc0 in main /root/Fuzzer/FuzzerMain.cpp:20:10
#13 0x7f5f5d99882f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
#14 0x41cad8 in _start (/root/heartbleed/openssl-1.0.1f+0x41cad8)


SUMMARY: AddressSanitizer: heap-buffer-overflow (/root/heartbleed/openssl-1.0.1f+0x4bcfe4) in >__asan_memcpy
Shadow bytes around the buggy address:
0x0c527fff9290: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c527fff92a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c527fff92b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c527fff92c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c527fff92d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c527fff92e0: 00 00 00 00 00 00 00 00 00[fa]fa fa fa fa fa fa
0x0c527fff92f0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c527fff9300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c527fff9310: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c527fff9320: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c527fff9330: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
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
Right alloca redzone: cb
==2674:2674==ABORTING
MS: 5 ChangeBit-ChangeBinInt-ChangeBinInt-ChangeBit-ChangeByte-; base unit: >70ba9446e37cb58654b50e7c1995484b01b173b7
0x18,0x3,0x0,0x0,0x2d,0x1,0x5a,0x0,0x28,0x3b,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x2,0x0,0x0,0x3,0x0,0x0,0x20>,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x3b,0x1,0x91,0xa,0x47,0x27,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x1,0x0,0x0,0x0,>0x79,0x0,0x0,0x0,0x20,0x0,0x0,0x3,0x0,
\x18\x03\x00\x00-\x01Z\x00(;\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x03\x00\x00 >\x00\x00\x00\x00\x00\x00\x00;\x01\x91\x0aG’\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00y\x00\x00\x00 >\x00\x00\x03\x00
artifact_prefix=’./’; Test unit written to ./crash-e0773874e471c18218f82d9b4b70e2fd467d86ab
Base64: GAMAAC0BWgAoOwAAAAAAAAAAAgAAAwAAIAAAAAAAAAA7AZEKRycAAAAAAAAAAAEAAAB5AAAAIAAAAwA=

출력된 결과 중에서 다음의 값들을 이용하여 소스코드를 확인해보자.

#1 0x4f8232 in tls1_process_heartbeat /root/heartbleed/BUILD/ssl/t1_lib.c:2586:3
#2 0x568cd2 in ssl3_read_bytes /root/heartbleed/BUILD/ssl/s3_pkt.c:1092:4
#3 0x56d4b1 in ssl3_get_message /root/heartbleed/BUILD/ssl/s3_both.c:457:7
#4 0x536599 in ssl3_get_client_hello /root/heartbleed/BUILD/ssl/s3_srvr.c:941:4
#5 0x532642 in ssl3_accept /root/heartbleed/BUILD/ssl/s3_srvr.c:357:9

먼저 소스 탐색이 용이하도록 몇 가지 도구들을 설치하자. 그리고 Dockerfile에 포함하여 퍼저 이미지에 항상 포함하여 사용하자.

apt-get install vim ctags cscope 

cscope를 이용하여 tls1_process_heartbeat함수를 호출하는 위치로 살펴보면 ssl/ssl_locl.h 파일의 1108을 찾을 수 있다.

1108 #ifndef OPENSSL_NO_HEARTBEATS
1109 int tls1_heartbeat(SSL *s);
1110 int dtls1_heartbeat(SSL *s);
1111 int tls1_process_heartbeat(SSL *s);
1112 int dtls1_process_heartbeat(SSL *s);
1113 #endif

전처리문으로 OPENSSL_NO_HEARTBEATS flag가 설정되어 있다면 tls1_process_heartbeat함수를 호출하는 것이다. 그래서 HeartBleed 취약점 존재 유무를 확인 시 openssl version -a| grep -oE '1.0.1[a-g]{1}?|DOPENSSL_NO_HEARTBEATS' 명령어를 이용하게 된다.

이제 tls1_process_heartbeat함수를 살펴보자.

2553 int
2554 tls1_process_heartbeat(SSL *s)
2555   {
2556   unsigned char *p = &s->s3->rrec.data[0], *pl;
2557   unsigned short hbtype;
2558   unsigned int payload;
2559   unsigned int padding = 16; /* Use minimum padding */
2560 
2561   /* Read type and payload length first */
2562   hbtype = *p++;
2563   n2s(p, payload);
2564   pl = p;

인자 값으로 전달 받은 변수 s를 이용하여 메시지 유형을 hbtype 변수에 저장하고 포인터를 1 바이트 증가시킨 후, n2s() 매크로로 payload에 16비트 Heartbeat payload 길이를 기록하고 포인터를 2 바이트 씩 증가시킵니다. 그러면 pl은 페이로드의 내용에 대한 포인터가됩니다.

/ssl/ssl_locl.hn2s macro

 249 #define n2s(c,s)  ((s=(((unsigned int)(c[0]))<< 8)| \
 250           (((unsigned int)(c[1]))    )),c+=2)

이어서 tls1_process_heartbeat함수에서 를 살펴보자.

2583     /* Enter response type, length and copy payload */
2584     *bp++ = TLS1_HB_RESPONSE;
2585     s2n(payload, bp);
2586     memcpy(bp, pl, payload);

ssl/ssl_locl.hs2n macro

 251 #define s2n(s,c)  ((c[0]=(unsigned char)(((s)>> 8)&0xff), \
 252         c[1]=(unsigned char)(((s)    )&0xff)),c+=2)

응답 유형을 버퍼 시작 부분에 기록하고 버퍼 포인터를 증가 시키며 s2n () 매크로를 사용하여 메모리에 16 비트 Heartbeat payload 길이를 기록하고 버퍼 포인터를 2 바이트 씩 증가시킨다. 그 다음, 수신 된 페이로드에서 응답 페이로드로의 바이트 수를 복사한다.

Created with Raphaël 2.1.2UserUserServerServerHackerHackerHeartbeat Message: NormalPayload: 6NormalHeartbeat Message: NormalPayload: 600Normal|<Privacy Data>|<Password>| ...

페이로드는 사용자에 의해 제어되어 실제로 보낸 Heartbeat Message가 1 바이트의 페이로드 만 있고 payload_length가 거짓이면 위의 memcpy ()는 수신 된 HeartbeatMessage의 끝 부분을 읽은 후, 대상 프로세스의 메모리에서 나머지를 읽게 된다. 이 메모리에는 암호 나 다른 클라이언트의 복호화된 메시지와 같은 중요 정보가 들어 있다. 반복해서 Heartbeat Message를 보내면 또 다른 64KB가 누출되므로 중요정보가 탈취될 위험에 빠지게 된다.

Fix

OpenSSL 1.0.1g에서 payload 길이를 검증하는 로직을 추가하는 것으로 취약점은 조치되었다.

hbtype = *p++;
n2s(p, payload);
if (1 + 2 + payload + 16 > s->s3->rrec.length)
    return 0; /* silently discard per RFC 6520 sec. 4 */
pl = p;

여기까지 LIBFUZZER를 이용하여 HeatBleed 취약점을 알아보았다. LIBFUZZER는 비교적 사양이 낮은 노트북으로 짧은 시간 안에 퍼징 테스트를 할 수 있겠다는 장점에서 활용방법을 익히기 시작하였다. 사용 결과 예상했던 것보다 더 장점이 많은 도구임을 알 수 있었으며 어느정도 숙련되었을 때, LIBFUZZER를 이용하여 단위 테스트를 진행한다면 소프트웨어 보안수준 향상에 많은 도움이 될 것이라 생각하였다.

이 블로그의 인기 게시물

데일 카네기 인간관계론 정리

Remove-Server-Header

응답 메시지 내 서버 버전 정보 제거 1. Apache 1) 조치 방법 “/etc/htpd/conf/httpd.conf” 파일 안에서 1. ServerTokens OS → ServerTokens Prod 2. ServerSignature On → ServerSignature Off 로 변경한 후 아파치를 재시작하면 헤더 값의 아파치 버전 정보 및 OS 정보를 제거할 수 있다. 2) 참고 URL http://zetawiki.com/wiki/CentOS_ 아파치_보안권장설정_ServerTokens_Prod,_ServerSignature_Off 2. IIS 1) 조치 방법 IIS 6.0 urlscan_setup 실행. 설치. \windows\system32\inetsrv\urlscan\urlscan.ini 파일을 열어 다음 수정(RemoveServerHeader=0 을 RemoveServerHeader=1 로 변경) 서비스에서 IIS Admin Service 재시작. IIS 7.0 IIS 관리자를 열고 관리하려는 수준으로 이동합니다. 기능 보기에서 HTTP 응답 헤더를 두 번 클릭합니다. HTTP 응답 헤더 페이지에서 제거할 헤더를 선택합니다. 작업 창에서 제거를 클릭하고 예를 클릭합니다. 2) 참고 URL IIS 6.0 : http://gonnie.tistory.com/entry/iis6- 응답헤더-감추기 IIS 7.0 : https://technet.microsoft.com/ko-kr/library/cc733102(v=ws.10).aspx 3. jetty 1) 조치 방법 “jetty.xml” 파일에서 jetty.send.server.version=false 설정 2) 참고 URL http://attenuated-perspicacity.blogspot.kr/2009/09/jetty-61x-hardening.html 4. Nginx ...

American Fuzzy Lop And Address Sanitizer

American Fuzzy Lop And Address Sanitizer Address Sanitizer(ASAN) 단지 gcc/clang에 -fsanitize=address 옵션을 추가하는 것으로 간단히 사용할 수 있지만 그 효과는 충분하다. Example Out Of Bounds Read #include <stdio.h> int main() { int a[ 2 ] = { 3 , 1 }; int i = 2 ; printf ( "%i\n" , a[i]); } 예제 파일을 OutOfBoundsRead.c 로 생성하고 ASAN 옵션을 지정하여 clang 으로 컴파일하자. clang -g -fsanitize = address -fno -omit -frame -pointer OutOfBoundsRead . c -o OutOfBoundsRead 생성된 OutBoundsRead 파일을 실행하면 다음과 같은 결과를 볼 수 있다. ================================================================= ==3678==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffc0ba87428 at pc 0x47b7db bp 0x7ffc0ba87390 sp 0x7ffc0ba87388 READ of size 4 at 0x7ffc0ba87428 thread T0 ==3678==WARNING: Trying to symbolize code, but external symbolizer is not initialized! #0 0x47b7da (/root/ASAN/OutOfBoundsRead+0x47b7da) #1 0x7faba0260f44 (/lib/x86_64-linux-gnu/libc.so.6+0x21f44) #2 0x...