Monthly Archives: September 2014

[번역] PHP 5.6에서부터 타이밍 공격에 대한 대응을 지원합니다.

원본글 : http://blog.ohgaki.net/php-5-6-become-timing-safe

PHP 5.6부터는 ‘타이밍 공격’에 대한 대책이 도입되었습니다. 메이저 어플리케이션에는 타이밍 공격에 대한 대책이 도입되어 있습니다만, PHP 5.6에서부터는 간단하게 공격에 대응할 수 있게 되었습니다.

타이밍 공격이란 컴퓨터가 동작하는 시간의 차이를 측정하여 공격에 이용하는 수법입니다. 알고리즘 자체의 취약성이 아닌 컴퓨터의 동작 시간, 온도, 소리, 전자적 노이즈, 전력 사용량 등 부가적인 현상을 공격의 도구로 활용하는 ‘사이드 채널 공격’의 일종입니다. 예를 들면 블라인드 SQL 인잭션 기법에서는 타이밍 공격이 대단히 유용하다는 것이 이전부터 알려져 왔습니다.

다음과 같은 코드가 타이밍 공격에 취약한 코드 입니다.

<?php

if( $screct_password === $_GET['password'] )
{
  $authenticated = TRUE;
}

이 코드는 평문 패스워드($secret_password)를 유저가 송신한 패스워드($_GET[‘password’)와 일치하는지 확인하고 일치하는 경우 인증값($authenticated)를 TRUE로 설정합니다.

패스워드가 해싱되지 않는 평문인 것을 제외하면 특별히 이상할 것이 없는 코드 입니다만, 이 코드는 타이밍 공격에 취약합니다. 이유를 알기 위해서는 PHP내부에서 이 코드가 어떻게 동작하는지를 알 필요가 있습니다.

문자열 === 문자열 or 문자열 == 문자열

위와 같이 비교할 때 문자열의 길이가 다른 경우 PHP는 곧바로 false를 반환합니다. 문자열의 길이가 같은 경우에만 표준 C 라이브러리의 strncmp를 이용하여 문자열을 비교합니다.

길이가 다른 경우 strncmp가 호출되지 않기 때문에 길이의 일치 여부에 따라 미묘하게 응답의 시간차가 발생합니다. 응답시간의 차이를 통계적으로 처리하면 패스워드의 길이를 파악할 수 있습니다.

평문 패스워드의 길이를 확인하면 이번에는 strncmp의 동작시간의 차이를 계측합니다. strncmp는 비교하는 문자열이 다르면 곧바로 false를 반환합니다. 즉, 최초 첫문자가 일치하는 경우와 그렇지 않은 경우 역시 미묘한 응답시간의 차이가 발생합니다. 이런 작업을 반복하여 1문자씩 패스워드를 해독해갑니다. 패스워드가 랜덤하게 생성된 문자열이라고 해도 난이도는 크게 변하지 않습니다.

알기쉬운 예를 들면 패스워드가 숫자로만 이루어져있고 최대 길이가 6자리인 경우 경우의 수는

10^6 = 1,000,000

입니다. 하지만 타이밍 공격을 이용할 경우

10*6 = 60

의 경우의 수로 해석이 가능합니다. 물론 타이밍 공격을 실시하는 경우 처리 시간의 차이를 통계적으로 처리해야 하기 때문에 60번 이상의 엑세스를 시도해야 할 필요가 있습니다. 가령 1문자당 1,000회의 엑세스를 한다고 한다면 6만회의 시도가 필요합니다. 모든 경우의 수를 고려하는 것과 비교해 압도적으로 적은 횟수로 해석이 가능합니다.

해싱된 값 또한 타이밍 공격에 비슷한 취약점을 갖습니다. 이러한 미묘한 시행시간의 차이를 이용하여 실제로 Web 어플리케이션에 대한 공격이 가능할까? 라는 의문을 갖는 분도 계실지 모르겠습니다만 이미 가능하다는 것이 실제로 증명되었습니다. 네트워크 지연 등 우연적으로 발생하는 부분에 대해서 통계적으로 처리해야할 부분이 늘어나긴 합니다만 최근 네트워크 기술의 발달로 인해 이런 우연의 요소가 비교적 많이 감소하여 타이밍 공격이 더욱 용이하게 되었습니다.

로그인 패스워드 등은 연속으로 틀리는 경우에 대한 제한을 둘 수 있으나 API키의 대량의 엑세스를 전제로하는 서비스의 경우에는 엑세스 제한을 두는 것은 현실적으로 불가능 합니다. 가령 1일 최대 엑세스 한도를 설정하더라도 몇일의 시간을 두고 시도한다면 해석이 가능합니다.

OS 및 다른 언어에서의 대책

예를들면 Python 3.4부터는 문자열 비교에 해쉬값을 이용합니다. OS에서도 유사한 라이브러리를 제공합니다. 예를들면 OpenBSD의 timingsafe_bcmp 등이 있습니다.

int
timingsafe_bcmp(const void *b1, const void *b2, size_t n)
{
  const unsigned char *p1 = b1, *p2 = b2;
  int ret = 0;

  for (; n &gt; 0; n--)
    ret |= *p1++ ^ *p2++;
  return (ret != 0);
}

C언어를 모르더라도 어떤 동작을 하는 코드인지 알 수 있으리라 생각합니다. 고정길이의 데이터에 대해 1바이트씩 모든 바이트를 비교합니다. 어떤 입력값이 들어오더라도 항상 같은 동작을 함으로 처리 시간의 차이를 이용한 타이밍 공격을 무효화 합니다.

Python 3.4에서는 SipHash라고 하는 해쉬를 생성하는 알고리즘을 이용하여 해쉬를 생성하여 64비트 정수로서 비교합니다. 해쉬 함수를 이용해 그 결과를 비교하면 타이밍 공격을 무효화합니다.

PHP에서의 대책

PHP 5.6에서 부터는 타이밍 공격에 대한 대책이 도입되었습니다.

http://kr1.php.net/manual/en/function.hash-equals.php

기타 대책

스스로 대책을 강구해 볼 수도 있습니다만 프레임워크에서 사양으로 제공하는 경우도 있습니다.

  • Symfony
  • ZendFramework
  • Joomla Framework

PHP의 hash_password 함수를 이용하는 경우 타이밍 공격으로부터 안전합니다. 패스워드는 통상 해쉬화한 패스워드를 비교하기 때문에 타이밍 공격을 이용할 수 없습니다. API키 등 문자열로서 보존하고 비교하는 경우에는 주의할 필요가 있습니다. 상기한 프레임워크가 제공하는 문자열 비교방법을 이용하거나 직접 해쉬값을 비교하셔야 합니다.

API키 등 열쇠가 되는 해쉬값이 특정하기 쉬운 조건은 다음과 같습니다.

  • 열쇠가 특정 유저의 ID와 연관되어 있다
  • 열쇠가 메모리에 캐쉬되어 있다
  • 열쇠를 1문자씩 비교한다.

이경우 문자열 비교의 미묘한 지연시간을 해석해서 공략할 수 있는 가능성이 높아집니다. 1문자씩 비교하는 문자열 비교를 배제하지 않으면 공략될 가능성이 남기때문에 문자열 비교시간을 일정하게 유지하는 것이 중요합니다. 앞서 말한 해쉬함수를 이용하여 유저가 송신한 문자열을 비교하는 것이 안전합니다.

열쇠가 특정 유저 ID와 연관되어 보존되어 있는 경우란 유저 ID와 열쇠가 같이 송신되는 경우를 말합니다. 예를들면 다음과 같은 요청입니다.

http://example.com/api.php?userid=1234&apikey=123456789

타이밍 공격에 안전한 비교 방법은 열쇠가 되는 문자열의 길이를 노출시키지 않는 것이지만 사실상 문자열의 길이를 안전하게 은폐하는 것은 불가능합니다. 열쇠의 길이를 알더라도 안전하도록 충분히 긴 값을 이용하는 것이 타이밍 공격의 리스크를 줄일 수 있습니다.

정리

타이밍 공격은 언어나 OS에 관계없이 적용가능한 공격방법 입니다. PHP 이외의 언어에서도 마찬가지입니다. 보안이 중요한 시스템에서는 타이밍 공격의 리스크를 무시해서는 안됩니다. 보호된 환경에 놓인 어플리케이션이라고 해서 안심해서는 안됩니다. 보호된 환경에서도 SSRF 공격의 가능성은 항상 존재하며, 보호된 시스템 일수록 일반에 공개된 시스템과 비교해 반응시간에 개입하는 우연적인 요소가 적기 때문에 타이밍 공격이 더 용이할 것으로 판단됩니다.

마지막으로 우연적 요소를 증가시키를 것을 대응방법으로 삼는 것은 추천하지 않습니다. 우연성을 높이는 것은 완화책으로서는 기능하지만 근본적인 해결책은 될 수 없습니다. 오직 근본적인 대책만이 보안수단은 아니지만 문자열 비교의 처리시간을 일정시간으로 유지하는 것이 보다 우수하고 현실적인 방법입니다.