티스토리 뷰

전 세계 인터넷은 OpenSSL의 중대한 버그로 난리다. 이 버그의 주요 요지는 특정 OpenSSL을 사용할 경우 메모리의 64KB를 획득할 수 있고, 이 버그로 서버 인증서의 비밀키(개인키)를 취득하여 서버로 오가는 모든 패킷을 취득할 수 있다. 라는 것인데, 이를 가리켜 Heartbleed(심장출혈) 버그라고 한다.

그 만큼 심각한 버그가 맞는데, 일각의 미디어에서 최악의 시나리오를 너무 일반화시키는 것이 아닌가 싶다.

필자가 보안 전문가는 아닌 만큼 잘못된 부분은 너그러이 지적해 주길 바란다.

- 사용자는 모두 패스워드를 변경해야 하나?

해도 되고 안해도 된다. 어차피 개인정보는 오픈소스. :)

농담이고, 취약성이 있는 OpenSSL을 사용하면 공격자는 클라이언트의 사용자 요청 데이터를 가로챌 수 있다.

공격자가 사용자의 패스워드를 가로채려면, 서버 인증서의 비밀키를 취득한 경우에 해당 된다. 하지만 heartbleed 버그는 인증서의 비밀키가 없이도 서버의 메모리 최대 64KB를 볼 수 있다. 모든 데이터를 가로채는 것은 아니고 64KB에 해당하는 찌꺼기(?) 영역인데, 이 영역에 마지막 클라이언트의 요청 데이터가 저장되어 있다.

그러므로 자주 방문하는 사이트면 비밀번호를 변경하는 것이 좋고, 1년 넘게 방문하지 않은 사이트는 변경하지 않아도 된다.

- 서버 운영자는 인증서를 모두 폐기해야 하나?

취약한 버전의 OpenSSL을 사용하고, 최근 서버를 restart 한 경우가 아니라면 거의 제로(0)에 가깝다.

공격자가 64KB 중에서 인증서의 비밀키를 훔치기 위해서는 대상 서버를 재가동하고 첫 번째 요청인 경우에 이 비밀키를 훔쳐갈 수 있는 가능성이 높아진다고 한다. 이는 Answering the Critical Question: Can You Get Private SSL Keys Using Heartbleed? 에서 실험한 결과이다.

- OpenSSL 업그레이드가 불가능할 경우

소스 코드를 보면 곳곳에 아래와 같이 #ifndef OPENSSL_NO_HEARTBEATS 지시자를 발견 할 수 있다. 그러므로 OpenSSL 을 OPENSSL_NO_HEARTBEATS 옵션과 함께 다시 컴파일 하면 heartbleed 취약성 버그를 해결할 수 있다.

#ifndef OPENSSL_NO_HEARTBEATS  
int  
tls1_process_heartbeat(SSL *s) {  
...  
...  
}  

int  
tls1_heartbeat(SSL *s) {  
...  
...  
}  
#endif  

- OpenSSL 코드 품질

Heartbleed 취약성 버그가 해결된 7e840163 커밋을 살펴보면, 아직도 여전히 코드 리뷰를 통해 이슈가 남아있다.

코드 측면에서 변수의 이름이 'payload 는 payload_length 가 되어야 하지 않느냐' 라는 의견이 있다. 그리고 padding 값이 16으로 초기화가 되었음에도 곳곳에 하드 코딩된 '16' 값을 찾아볼 수 있다.

가장 최신 커밋에는 코드 리뷰가 완료되었는 지 모르겠으나, 당시 취약성 버그로 상당히 급하게 코드를 수정한 것 같다는 느낌을 받을 수 있었다.

OpenSSL 디버깅

Heartbeat 프로토콜 Heartbeat network, Linux-HA에서 알 수 있듯이 클러스터링(clustering) 및 고가용성(high-availability linux)을 위해 서버끼리 주고 받는 메시지라고 한다. active, standby 서버 두 대 중 active 서버가 죽으면 standby 가 가동되어 장애를 최소화 하는데, 이 때 ‘죽었니 살았니’ 빼꼼 찔러보는 걸 heartbeat 라고 한다고 한다.

실제 필자의 클라우드 서버에서 테스트를 진행하려고 했으나 여건이 되지 않아 실제 환경과 유사하게 테스트는 하지 못했다.

먼저, github의 OpenSSL 소스 코드를 받고, 버그가 있는 tag 및 branch를 checkout 한다. 이어 디버그 모드로 컴파일을 하면 테스트 준비는 완료된다. 그리고 heartbeat 패킷을 보내줄 수 있는 github의 pacemaker 클라이언트 코드를 받는다.

호스팅된 openssl, lldb attaching

디버그 모드로 컴파일된 openssl 을 self-hosting으로 실행한다.

$ lldb openssl s_server -www  

그리고 openssl/ssl/t1_lib.c 소스 코드의 tls1_process_heartbeat 함수에 브레이크 포인트를 걸었다.

(lldb) br list  
Current breakpoints:  
1: name = 'tls1_process_heartbeat', locations = 1, resolved = 1, hit count = 3  
  1.1: where = openssl`tls1_process_heartbeat + 21 at t1_lib.c:2484, address = 0x000aff55, resolved, hit count = 3  

이제 pacemarker 를 통해 heartbeat를 보냈다.

$ ./heartbleed.py -p 4433 -t 100000 localhost  

t1_lib.c 로컬 변수

다음의 코드 중 &s->s3->rrec.data[0]는 incoming 데이터가 포함 된다.

int  
tls1_process_heartbeat(SSL *s)  
    {  
    unsigned char *p = &s->s3->rrec.data[0], *pl;  
    unsigned short hbtype;  
    unsigned int payload;  
    unsigned int padding = 16; /* Use minimum padding */  

함수의 매개변수로 SSL *s 구조체의 s3 구조체의 데이터는 다음과 같다. s3->rrec가 클라이언트에서 보낸 데이터가 되겠다. 이 구조체는 다음과 같은 값을 가지고 있다.

(lldb) e *s->s3  
(ssl3_state_st) $58 = {  
  flags = 0  
  delay_buf_pop_ret = 0  
  read_sequence = ""  
  read_mac_secret_size = 0  
  read_mac_secret = ""  
  write_sequence = ""  
  write_mac_secret_size = 0  
  write_mac_secret = ""  
  server_random = "SN\x91ki��E\x82V\x01%E\v[t�7�\x91\x88\x9e[�\x8af\x95\x92iU"  
  client_random = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"  
  need_empty_fragments = 0  
  empty_fragment_done = 0  
  init_extra = 0  
  rbuf = (buf = "\x16\x03\x01\x18\x03\x01", len = 17736, offset = 11, left = 0)  
  wbuf = (buf = "", len = 17584, offset = 12, left = 0)  
  rrec = (type = 24, length = 3, off = 0, data = "\x01��|\x03\x01BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", input = "\x01��|\x03\x01BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", comp = 0x00000000, epoch = 0, seq_num = "")  
  wrec = (type = 22, length = 9, off = 0, data = "\x0e", input = "\x0e", comp = 0x00000000, epoch = 0, seq_num = "")  
  alert_fragment = ""  
  alert_fragment_len = 0  
  handshake_fragment = ""  
  handshake_fragment_len = 0  
  wnum = 0  
  wpend_tot = 4  
  wpend_type = 22  
  wpend_ret = 4  
  wpend_buf = 0x0236c800 "\x0e"  
  handshake_buffer = 0x00000000  
  handshake_dgst = 0x007071e0  
  change_cipher_spec = 0  
  warn_alert = 0  
  fatal_alert = 0  
  alert_dispatch = 0  
  send_alert = ""  
  renegotiate = 0  
  total_renegotiations = 0  
  num_renegotiations = 0  
  in_read_app_data = 0  
  client_opaque_prf_input = 0x00000000  
  client_opaque_prf_input_len = 0  
  server_opaque_prf_input = 0x00000000  
  server_opaque_prf_input_len = 0  
  tmp = {
    cert_verify_md = ""
    finish_md = ""
    finish_md_len = 0
    peer_finish_md = ""
    peer_finish_md_len = 0
    message_size = 124
    message_type = 1
    new_cipher = 0x002ca8d0
    dh = 0x00000000
    ecdh = 0x00706dc0
    next_state = 8576
    reuse_message = 0
    cert_req = 0
    ctype_num = 0
    ctype = ""
    ca_names = 0x00000000
    use_rsa_tmp = 0
    key_block_length = 0
    key_block = 0x00000000
    new_sym_enc = 0x00000000
    new_hash = 0x00000000
    new_mac_pkey_type = 0
    new_mac_secret_size = 0
    new_compression = 0x00000000
    cert_request = 0  
  }  
  previous_client_finished = ""  
  previous_client_finished_len = '\0'  
  previous_server_finished = ""  
  previous_server_finished_len = '\0'  
  send_connection_binding = 0  
  next_proto_neg_seen = 0  
}  

openssl 코드에서 2491: n2s(p, payload);가 클라이언트에서 요청한 payload 인데 이 값은 다음과 같다.

(lldb) fr v payload  
(unsigned int) payload = 65517  

실제 요청된 값과 길이를 체크하지 않은 채 아래와 같이 바로 메모리를 할당하게 되는데

   2505          * message type, plus 2 bytes payload length, plus
   2506          * payload, plus padding
   2507          */  
-> 2508         buffer = OPENSSL_malloc(1 + 2 + payload + padding);
   2509         bp = buffer;
   2510
   2511         /* Enter response type, length and copy payload */  

아래의 코드의 함수가 실행되면서, 위에서 할당된 메모리의 65536 bytes (=1+2+payload+padding) 를 buffer에 쓰면서 클라이언트로 64KB 의 over-read 된 메모리의 데이터까지 클라이언트에 response 된다.

   2516         /* Random padding */
   2517         RAND_pseudo_bytes(bp, padding);
   2518  
-> 2519         r = ssl3_write_bytes(s, TLS1_RT_HEARTBEAT, buffer, 3 + payload + padding);
   2520
   2521         if (r >= 0 && s->msg_callback)
   2522             s->msg_callback(1, s->version, TLS1_RT_HEARTBEAT,  
(lldb) fr v
(SSL *) s = 0x00469c10
(unsigned char *) p = 0x012f8a0b "|\x03\x01BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
(unsigned char *) pl = 0x012f8a0b "|\x03\x01BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
(unsigned short) hbtype = 1
(unsigned int) payload = 65517
(unsigned int) padding = 16
(unsigned char *) buffer = 0x01301600 "\x02��|\x03\x01BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
(unsigned char *) bp = 0x01301603 "|\x03\x01BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
(int) r = -1

메모리가 확보되는 buffer = OPENSSL_malloc(...)는 함수부 선언의 unsigned char *p = &s->s3->rrec.data[0]&p 메모리 위치 근처에(&p 주소보다 더 높은 주소) 확보가 된다. 위에서 &p는 incoming 데이터가 있다고 언급했다.

그러므로 heartbeat의 response의 값은 가장 최근에 남아 있는 incoming 데이터, 즉 클라이언트 요청 데이터의 찌꺼기가 남아있는데, over-read 버그로 인해 이 영역의 사용자 요청 데이터가 전송되게 된다.

이렇게 공격자가 훔친 데이터는 아래와 같이 클라이언트 요청 정보가 포함된다. 일반적으로 클라이언트가 서버로 요청하는 정보는 HTTP 프로토콜에 포함되는 URI, Header, Cookie 등을 가로챌 수 있다.

아래는 64KB 범위 안에서 클라이언트가 보낸 incoming 찌꺼기가 남은 64KB 메모리 값의 일부분이다.

(lldb) m r p --count 250  
0x0237300b: 7c 03 01 42 42 42 42 42 42 42 42 42 42 42 42 42  |..BBBBBBBBBBBBB  
0x0237301b: 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42  BBBBBBBBBBBBBBBB  
0x0237302b: 42 42 42 00 00 4e c0 30 c0 28 c0 14 00 9f 00 6b  BBB..N�0�(�....k  
0x0237303b: 00 39 00 88 c0 32 c0 2e c0 2a c0 26 c0 0f c0 05  .9..�2�.�*�&�.�.  
0x0237304b: 00 9d 00 3d 00 35 00 84 c0 12 00 16 c0 0d c0 03  ...=.5..�...�.�.  
0x0237305b: 00 0a c0 2f c0 27 c0 13 00 9e 00 67 00 33 00 45  ..�/�'�....g.3.E  
0x0237306b: c0 31 c0 2d c0 29 c0 25 c0 0e c0 04 00 9c 00 3c  �1�-�)�%�.�....<  
0x0237307b: 00 2f 00 41 01 00 00 05 00 0f 00 01 01 2f 32 30  ./.A........./20  
0x0237308b: 31 30 30 31 30 31 20 46 69 72 65 66 6f 78 2f 32  100101 Firefox/2  
0x0237309b: 38 2e 30 0d 0a 41 63 63 65 70 74 3a 20 74 65 78  8.0..Accept: tex  
0x023730ab: 74 2f 68 74 6d 6c 2c 61 70 70 6c 69 63 61 74 69  t/html,applicati  
0x023730bb: 6f 6e 2f 78 68 74 6d 6c 2b 78 6d 6c 2c 61 70 70  on/xhtml+xml,app  
0x023730cb: 6c 69 63 61 74 69 6f 6e 2f 78 6d 6c 3b 71 3d 30  lication/xml;q=0  
0x023730db: 2e 39 2c 2a 2f 2a 3b 71 3d 30 2e 38 0d 0a 41 63  .9,*/*;q=0.8..Ac  
0x023730eb: 63 65 70 74 2d 4c 61 6e 67 75 61 67 65 3a 20 6b  cept-Language: k  
0x023730fb: 6f 2d 6b 72 2c 6b 6f 3b 71 3d                    o-kr,ko;q=


댓글