오늘 팀 동료를 통해 bash 취약점이 있다는 내용을 들었고, cnet.com 링크를 통해 확인할 수 있다.

bash 보안 취약성 패치하기

제가 배치한 버전은 bash 4.3.25 (이 버전의 패치 코드 25개) 이며, 이 버그는 원격 코드 실행 버그로 매우 위협적인 버그이다.

맥에서는 당시(2014–09–26) MacPort, Homebrew 에서 패치 버전을 제공하지 않는 관계로, 직접 소스 코드를 컴파일 하는 방법으로 해결하였다.

최신 bash 버전으로 패치하고 컴파일 하는 스크립트 코드를 필자의 github 에 커밋하였다.
- https://github.com/powerumc/Patch-Bash-Vulnerability

맥/리눅스 에서 아래의 명령을 실행하면 바로 패치 하도록 했다.

curl https://raw.githubusercontent.com/powerumc/Patch-Bash-Vulnerability/master/patch-bash-4.3.25.sh | sh

참고 (버그 내용)

bash 원격 코드 실행 취약성

쉘에서 아래의 명령을 실행하여 echo 메시지가 확인되면 원격 실행 코드 버그가 있는 버전입니다.

$ env x='() { :;}; echo vulnerable' bash -c "echo this is a test"

코드 실행 결과 (원격 코드 실행 가능한 취약성)

vulnerable
echo this is a test

패치 후 원격 코드 실행 불가능

패치가 완료되면 원격가 실행되지 않는다.

$ env x='() { :;}; echo vulnerable' bash -c "echo this is a test"

코드 실행 결과

bash: warning: x: ignoring function definition attempt
bash: error importing function definition for \`x'
this is a test


Posted by 땡초 POWERUMC

댓글을 달아 주세요

  1. 이반린 2014.09.29 10:59 Address Modify/Delete Reply

    안녕하세요. 서버관리자 입니다. 제가 관리하는 서버가 BASH 취약점이 있는데요.
    외부적으로 포트를 열수가 없어서 수동으로 bash 쉘을 패치해야 하는데
    어떻게 해야 하는지 알려주시면 감사하겠습니다.
    부탁드립니다.

  2. 123124124 2014.09.29 14:59 Address Modify/Delete Reply

    RedHat, CentOS 일 경우
    rpm -Uvh bash............. 하시면 되요

  3. 최혁선 2014.09.29 15:42 Address Modify/Delete Reply

    안녕하세요. 영문페이지로가서 그대로 설치하였더니.....
    $ env x='() { :;}; echo vulnerable' bash -c "echo this is a test" 명령어를 입력하면 결과에
    sh: error importing function definition for `BASH_FUNC_module()'
    위에 오류가 납니다.. 모듈이 Import되어 있지 않다고 나오는것 같은데. 혹시 이런 증상 해결 방법이 있을까요? 쉘스크립트가 정상적으로 실행이 되는지 테스트 해봐야겠네요..

전 세계 인터넷은 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=


Posted by 땡초 POWERUMC

댓글을 달아 주세요