
전 세계 인터넷은 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=
댓글을 달아 주세요
안녕하세요. 서버관리자 입니다. 제가 관리하는 서버가 BASH 취약점이 있는데요.
외부적으로 포트를 열수가 없어서 수동으로 bash 쉘을 패치해야 하는데
어떻게 해야 하는지 알려주시면 감사하겠습니다.
부탁드립니다.
RedHat, CentOS 일 경우
rpm -Uvh bash............. 하시면 되요
좋은 정보 고맙습니다. ^^
안녕하세요. 영문페이지로가서 그대로 설치하였더니.....
$ env x='() { :;}; echo vulnerable' bash -c "echo this is a test" 명령어를 입력하면 결과에
sh: error importing function definition for `BASH_FUNC_module()'
위에 오류가 납니다.. 모듈이 Import되어 있지 않다고 나오는것 같은데. 혹시 이런 증상 해결 방법이 있을까요? 쉘스크립트가 정상적으로 실행이 되는지 테스트 해봐야겠네요..
저도 처음 보는 오류 메시지네요.
혹시 해결하시면 결과 공유 부탁 드릴게요.