8.4. 데이타 포맷팅을 제어해라 (문자열 포맷)

컴퓨터 언어에서 많은 출력 루틴들은 생성되는 포맷을 제어하는 매개변수를 갖고 있다. C 에서 가장 명백한 예는 printf(), sprintf(), fprintf() 등을 포함한 printf() 루틴 계열들로 다른 예는 시스템 로그 정보 작성 syslog() 와 프로세스 식별자 정보를 표시하는데 사용되는 문자열 설정 setproctitle() 이다. ``err" 또는 ``warn" 으로 시작하거나 ``log" 를 포함하거나 또는 "printf" 로 끝나는 이름을 갖는 많은 함수들은 고려할만한 가치가 있다. 파이썬에는 문자열에 대해 비슷한 방식으로 포맷팅을 제어하는 "%" 연산이 있다. 많은 프로그램들과 라이브러리들은 대개 내장 루틴 호출 및 추가적인 프로세싱 (예, glib 의 g_snprintf() 루틴) 을 함으로써 포맷팅 함수들을 정의한다.

뜻밖에도 많은 사람들은 이러한 포맷팅 능력의 중요성을 잊고 있으며 신뢰되지 않은 사용자들로부터의 데이타를 포맷팅 매개변수로 사용하고 있는 것 같다. 신뢰되지 않은 사용자로부터의 필터링되지 않은 데이타를 포맷 매개변수로 절대로 사용하지 마라. 아마도 이는 다음 예에서 가장 잘 볼 수 있다:
  /* 잘못된 방식: */
  printf(string_from_untrusted_user);
  /* 옳은 방식: */
  printf("%s", string_from_untrusted_user); /* 안전하다 */
  fputs(string_from_untrusted_user); /* 단순 문자열의 경우 더욱 좋다 */

그렇지 않으면 공격자는 포맷팅 문자열을 주의깊게 선택함으로써 모든 종류의 악영향을 일으킬 수 있는데 C 에서 printf() 의 경우가 좋은 예이다. printf() 에서 사용자 제어 포맷 문자열을 어떻게든 악용하는 많은 방법들이 있는데 이들로는 긴 포맷팅 문자열 생성에 의한 버퍼 오버런 (이는 공격자로 하여금 프로그램을 완전히 제어할 수 있게 한다), 전달되지 않은 매개변수를 사용하는 변환 선언 (예기치 않은 데이타를 삽입시킨다) 과 전혀 예상치 못한 결과값들을 산출하는 포맷 생성하기 (가령 위험한 데이타를 앞 또는 뒤에 덧붙임으로써 가능한데 추후 사용시 문제를 일으킨다) 들이 있다. 특히 난처한 경우는 printf 의 "%n" 변환 선언으로 이는 지금까지 작성된 문자들의 수를 포인터 인수내로 쓰는데 공격자는 이를 악용하여 인쇄할 값을 겹쳐쓸 수 있다. 공격자는 실제로 전달되지 않는 매개변수를 지정할 수 있기 때문에 거의 임의의 위치에 언제나 겹쳐쓸 수 있다. 많은 논문들은 이러한 공격들을 더욱 자세하게 논의하는데 예를 들어 Avoiding security holes when developing an application - Part 4: format strings를 보라.

많은 경우 결과들은 사용자에게 되돌려지기 때문에 이 공격은 스택에 대한 내부 정보를 드러내는데 사용될 수 있다. 이 정보는 추후 StackGuard 와 같은 스택 보호 시스템을 뚫는데 사용될 수 있다; StackGuard 는 공격을 탐지하기 위해 상수 ``canary" 값들을 사용하는데 스택의 내용들이 표시된다면 canary 의 현재 값이 노출되어 공격에 취약할 것이다.

포맷팅 문자열은 국제화에 대한 검색 (look) 을 구현하기 위한 함수 호출 (gettext's _() 를 통해) 을 될 수 있는 한 포함하여 거의 언제나 상수 문자열이여야 한다. 이 검색은 프로그램이 제어하는 값들로 한정되어야 함을 주목해라, 즉 사용자는 프로그램이 제어하는 메시지 파일들로부터만 선택할 수 있어야 한다. 사용자 데이타를 사용하기 전에 이를 필터링하는 것 ([A-Za-z0-9] 와 같은 포맷 문자열에 대해 합법적인 문자들을 열거하는 필터를 설계함으로써) 은 가능하지만 보통 상수 포맷 문자열 또는 fput() 을 대신 사용함으로써 문제를 단순히 예방하는 것이 일반적으로 더욱 좋다. 저자는 이를 "출력" 문제로 열거했지만 출력 루틴들이 파일에 저장하거나 또는 snprintf() 를 통해 그와 같은 내부 상태를 생성할 수도 있기 때문에 이는 출력전에 프로그램에 내부적으로 문제를 일으킬 수 있다.

보안 문제를 야기하는 입력 포맷팅 문제는 사용되고 있지 않은 가능성은 아닌데 이 약점을 이용해 악용한 예를 CERT Advisory CA 2000-13 에서 보라. 이러한 문제가 어떻게 악용될 수 있는 지에 대한 더욱 자세한 정보는 Bugtraq 의 2000년 6월 18일 판에 출판된 Pascal Bouchareine 의 "[Paper] Format bugs" 라는 이메일 기사를 보라. 2000년 12월 현재 gcc 컴파일러 개발 버전은 개발자가 이러한 문제들을 피할 수 있도록 비보안적인 포맷 문자열 사용에 대해 경고 메시지를 지원하고 있다.

물론 이 모두는 국제화 검색이 실제 보안적인지 아닌지에 대한 논점을 교묘히 피하는 것인데 각자의 국제화 검색 루틴들을 작성하려면 반드시 신뢰되지 않은 사용자는 단지 합법적인 로케일만 지정할 수 있으며 임의의 경로와 같은 것들을 지정할 수 없도록 해라.

분명히 각자는 국제화를 통해 생성된 문자열을 믿을 수 있는 문자열로 제한하길 원하는데 그렇지 않다면 공격자는 이 능력을 사용해 포맷 문자열에서의 약점, 특히 C/C++ 프로그램에서의 약점을 악용할 수 있다. 이는 Bugtraq 내의 쟁점이었는데 (2000년 6월 26 일 올려진 John Levon 의 Bugtraq 게시물을 보라) 더욱 자세한 정보를 위해서는 4.7.3절 에서 합법적인 언어 값들만을 사용자가 선택하도록 허용하는 것에 대한 논의를 보라.

실제로 프로그래밍 버그일지라도 다른 국가들은 다른 방식으로 숫자들을 표시한다고 언급하는 것은 가치가 있는데 특히 마침표 (.) 와 쉼표 (,) 모두 소수부로부터 정수를 분리하기 위해 사용되고 있다. 데이타를 저장 또는 적재하려면 반드시 사용 로케일이 데이타 처리를 간섭하지 않도록 할 필요가 있다. 그렇지 않다면 저장된 데이타와 검색된 데이타가 다른 분리자를 사용할 것이기 때문에 프랑스어 사용자와 영어 사용자가 데이타를 교환할 수 없을 수도 있다. 이것이 보안 문제로 사용되고 있는 지는 모르지만 상상할 수 있는 일이다.