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, Size를 BIO_write함수의 인자 값으로 지정한 것을 볼 수 있다.
BIO_write(out, buf, len)
buf에서 len만큼 out에 write한다.
아래 명령어를 수행하면 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.h의 n2s 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.h의 s2n 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 바이트 씩 증가시킨다. 그 다음, 수신 된 페이로드에서 응답 페이로드로의 바이트 수를 복사한다.
페이로드는 사용자에 의해 제어되어 실제로 보낸 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를 이용하여 단위 테스트를 진행한다면 소프트웨어 보안수준 향상에 많은 도움이 될 것이라 생각하였다.