Using Heartbleed as a starting point

Salvatore Sanfilippo

Heartbleed를 출발점으로

원문은 Salvatore Sanfilippo님이 에 게재했습니다. 이 블로그 구독하기

최근 OpenSSL 버그에 대한 격한 반응은 이해할 만하다. 갑자기 인터넷 전체를 패치해야 하는 상황이 유쾌할 리 없으니까. 게다가 개인적으로는 버그가 얼마나 사소한지가 더 충격적이다. OpenSSL 개발자들을 비난하려는 건 아니지만, OpenSSL 같은 소프트웨어라면 보통 이런 종류의 문제는 좀 더 교묘할 거라고 생각하게 된다. 보통은 sanity check를 *제대로* 하지 못해서 실패하는 것이지, 이번 버그처럼 memcpy() 호출에 경계 검사가 *아예 없는* 경우는 아니기 때문이다.

하지만 가끔 아침에 전날 밤에 쓴 코드를 읽고 깊이 부끄러워질 때가 있다. 프로그래머는 실수를 한다, 나만 해도 자주 그러니, 필요한 건 다른 프로세스이지 다른 OpenSSL 팀이 아니라는 게 내 생각이다.

C보다 안전한 다른 언어를 쓰자고 주장하는 사람도 있고, 스펙이 너무 복잡해서 망가졌다고 주장하는 사람도 있다. 두 주장 모두 일리가 있겠지만, 우리가 곧 다른 스펙이나 시스템 언어로 옮겨갈 가능성은 낮다. 그러니 진짜 질문은, 시스템 소프트웨어의 보안을 지금 당장 개선하기 위해 무엇을 할 수 있는가이다?

1) 돈을 쏟아붓기.

시스템 코드를 더 안전하게 만드는 일은 투자가 있다면 간단하다. 여러 기업이 보안 전문가를 고용해 OpenSSL 코드베이스에 대한 코드 감사를 수행한다면 heartbleed 같은 버그를 발견할 확률은 커진다.

나는 자명하지 않은 조건들이 조합되어야 발동되는 매우 복잡한 버그들이 철저한 코드 감사 노력을 통해 발견되는 것을 봐왔다. 경계 검사 없는 memcpy() 같은 건 보안 관점에서 코드를 분석한다면 첫 번째 읽기에서 바로 눈에 띈다. 그리고 heartbleed가 어떻게 발견됐는지 아는가? Google에서 수행한 보안 감사 덕분이었다.

아마도 오픈소스를 그저 가져다 쓰는 대상으로만 여기던 시절은 끝났다. 많은 기업이 Google을 비롯한 다른 기업들의 사례를 따라 OSS 소프트웨어 개발과 보안을 위해 인력을 투입해야 한다.

2) 정적 및 동적 검사.

정적 코드 분석은 부수적으로 코드를 반자동으로 감사하는 방법이다. OpenSSL 같은 핵심 시스템 코드에서는 정적 분석을 더 효과적으로 만들기 위해 소스 코드에 어노테이션을 추가하거나 일련의 규칙을 사용하는 것조차 충분히 감수할 만하다.

오늘날 정적 분석 도구가 완전한 해결책은 아니지만, 전문 프로그래머가 정적 분석 결과를 꼼꼼히 검토한다면 어느 정도 가치를 제공할 수 있다.

또 다른 큰 도움은 Valgrind 같은 동적 검사에서 온다. C로 작성된 모든 시스템 소프트웨어는 매 새로운 커밋마다 Valgrind를 이용해 자동으로 테스트되어야 한다.

3) 라이브러리로 C를 추상화하기.

C는 로우레벨 언어이며 언어 자체에 내장된 안전장치가 없다. 하지만 C의 좋은 점 중 하나는 그 날것 위에 레이어를 쌓아 올릴 수 있는 언어라는 것이다.

제대로 된 동적 문자열 라이브러리는 수많은 버퍼 오버플로 문제를 예방하고, 오늘날 웬만한 제대로 된 프로젝트라면 하나쯤은 쓰고 있다. 하지만 그 이상으로 할 수 있는 일도 있다. 예를 들어 메모리에 private key 같은 것이 담길 수 있는 보안이 중요한 코드라면, 동적 문자열 라이브러리에 한 버퍼에서 다른 버퍼로 복사할 때 암묵적으로 sanity check를 수행하는 메모리 복사 프리미티브를 추가할 수 있다.

게다가 버퍼에 중요한 데이터가 담겨 있다면 논리적 권한을 설정해 이 영역에서 복사를 시도하면 프로그램이 중단되도록 할 수도 있다. 메모리 관리를 이용해 중요한 메모리 페이지를 더욱 효과적으로 보호하는, 이식성이 조금 떨어지는 다른 방법들도 있지만, 이식성/예측 가능성 문제 때문에 현실에서는 더 높은 수준의 C 레벨 보호가 훨씬 더 간단할 수 있다.

일반적으로 보호 장치 없이 C를 그대로 쓰는 것을 피하고, 그 위에 추상화 계층으로서 프로그래밍을 더 안전하게 만드는 라이브러리를 만드는 방향으로 탐구할 수 있는 방법은 많다.

4) 랜덤화된 테스트.

유닛 테스트로는 엣지 케이스나 실패한 sanity check를 촉발하기 어렵다. 수십 년 전부터 알려진 테스트 기법 중 하나가 있는데, 내 생각에는 충분히 활용되지 않고 있다. 바로 퍼즈 테스팅(fuzzy testing)이다.

OpenSSL 버그는 Valgrind 같은 동적 분석 도구와 함께 서로 다른 무작위 파라미터를 가진 다양한 종류의 OpenSSL 패킷을 보내면 분명히 발견할 수 있었던 버그였다.

내 경험상 수많은 랜덤화된 테스트와 함께, 프로그램이 Valgrind 위에서 동작하는 상태에서 동일한 테스트를 계속 반복 실행하는 환경을 갖추면 그렇지 않으면 눈에 띄지 않았을 현실 세계의 버그들을 상당수 발견할 수 있다. 탐구할 만한 모델은 많다. 보통은 완전히 무작위한 데이터를 주입하는 방식이 필요하고, 유효한 패킷을 여러 가지 무작위한 방식으로 손상시키는 중간 모델도 있다.

이 기법의 전형적인 예가 오래된 DNS 압축 무한 루프 버그다. 순진한 구현에 무작위 패킷 몇 개만 던져 보면 몇 분 안에 찾아낼 수 있다.

5) 보안 vs 성능에 대한 사고방식의 전환.

OpenSSL이 일부 시스템에서 malloc/free가 느리다는 이유로 자체 할당 캐싱을 하고 있다는 점은 흥미롭다. 이는 보안이 중요한 코드에서조차 여전히 성능이 안전보다 지나치게 존중받고 있다는 신호다. 이 특정 사례에 대해서는 OpenSSL 개발자들이 malloc을 래핑할 때 그렇게 함으로써 생기는 보안상의 함의를 생각하지 못했을 가능성이 크다는 점은 인정해야 한다. 하지만 그들이 *일부* 시스템에서의 할당 함수 같은 로우레벨 세부사항을 신경 썼다는 사실 자체가 성능에 대한 깊은 우려의 신호이며, 오히려 시스템의 정확성/안전성에 대해 더 깊이 우려했어야 한다는 뜻이다.

일반적으로 오늘날 서버 인프라의 사실상 표준 시스템인 Linux가, 찾아볼 수 있는 최악의 할당자 중 하나를 오랫동안 가져왔고 지금도 가지고 있다는 사실은 도움이 되지 않는다. 이는 대부분 라이선스 문제 때문인데, 더 나은 할당자들이 GPL이 아니라 BSD 라이선스이기 때문이다.

아마도 대기업들이 기여해야 할 또 다른 영역이 바로 glibc malloc을 크게 개선하는 일일 것이다. 더 나은 대안이 있음에도 불구하고 glibc malloc은 어차피 많은 현실 세계의 시스템 소프트웨어가 결국 사용하게 될 것이기 때문이다.

나는 heartbleed에 대한 논의가 좀 더 실용적인 접근을 취했으면 좋겠다. 한 가지 확실한 것은, 여기저기 비난하는 것으로는 OpenSSL이나 다른 무엇의 실제 보안 수준이 바뀌지 않는다는 것이며, 앞으로도 새로운 도전 과제들이 있기 때문이다. 예를 들어 HTTP/2.0 구현은 보안 측면에서 매우 민감한 순간이 될 수 있다.

수정: 사실 내가 틀렸고 Glibc 내부의 malloc 구현은 BSD 라이선스이므로 라이선스 문제는 아니다. 왜 Glibc가 매우 훌륭하고 활발히 개발되고 있는 할당자인 Jemalloc을 사용하지 않는지 모르겠다.

이 글은 muse-spark-1.2-contributor 모델을 사용해 번역했습니다.

댓글