5.2. C/C++ 에서의 라이브러리 솔루션

C/C++ 에서 한가지 해결방법은 버퍼 오버플로우 문제를 갖지 않는 라이브러리 함수를 사용하는 것이다. 첫 하부 절은 작동될 수 있지만 단점을 갖는 표준 C 라이브러리 솔루션을 기술하며 다음 절은 버퍼에 대한 고정된 길이 및 동적으로 재할당된 접근 방법에 대한 일반적인 보안 쟁점을 기술한다. 다음 하부 절은 strlcpy 와 libmib 와 같은 다양한 대체 라이브러리를 기술한다.

5.2.1. 표준 C 라이브러리 솔루션

C 에서 버퍼 오버플로우를 예방하는 표준적인 해결방법 (몇몇 C++ 프로그램에서도 사용된다) 은 이러한 문제에 대해 보호하는 표준 C 라이브러리 호출을 사용하는 것이다. 이러한 접근 방법은 표준 라이브러리 함수인 strncpy(3) 과 strncat(3) 에 매우 의존한다. 이 접근 방법을 선택한다면 다음에 주의해라: 이 호출은 약간은 놀랄만한 의미를 갖으며 적절히 사용하기가 어렵다. strncpy(3) 함수는 소스 문자열 길이가 적어도 수신지 문자열 길이와 같다면 수신지 문자열을 NIL 로 끝내지 않는데 따라서 strncpy(3) 을 호출한 후 수신지 문자열의 마지막 문자를 확실히 NIL 로 설정해라. 동일한 버퍼를 여러번 재사용하려고 하다면 효과적인 접근 방법은 버퍼가 실제보다 한 문자 짧으며 사용하기 전에 마지막 문자를 NIL 로 설정한다고 strncpy() 에 알려주는 것이다. strncpy(3) 과 strncat(3) 둘 모두는 사용할 수 있게 남아있는 공백을 건네줄 것을 요구하는데 이는 틀리기 쉬운 계산으로 이를 틀린다면 버퍼 오버플로우 공격을 허용할 것이다. 오버플로우가 일어났는 지를 결정하기 위한 단순한 메카니즘을 절대로 제공하지 마라. 마지막으로 strncpy(3) 은 strncpy(3) 이 수신지의 나머지를 NIL 로 채우기때문에 아마 대체되는 strcpy(3) 에 비해 성능면에서 상당한 불이익을 갖고 있다. 저자는 이러한 마지막 사항에 대해 놀라움을 표현한 이메일을 받았는데 이는 Kernighan 과 Ritchie 두번째판 [Kernighan 1988, page 249] 에 명확히 설명되어 있으며 이 동작은 리눅스, FreeBSD 및 솔라리스 맨페이지에 명백히 문서화되어 있다. 이는 단지 strcpy 대신 strncpy 를 쓰는 것은 대부분의 경우에 있어 어떠한 적당한 이유없이 성능면에서 가차없는 저하를 야기할 수 있음을 의미한다.

버퍼 오버플로우를 예방하는 동시에 sprintf() 를 사용할 수 있지만 이때 주의를 기울일 필요가 있다; 추천하기 어려울만큼 이를 잘못 적용하는 것이 쉽다. sprintf 제어 문자열은 다양한 변환 지정자 (예, "%s") 를 포함할 수 있으며 제어 지정자는 선택적인 필드 너비 (예, "%10s") 와 정확도 ("%.10s") 지정을 가질 수 있다. 이는 꽤 유사하게 보이지만 (단지 차이는 마침표이다) 이들은 매우 다르다. 필드 너비는 단지 최소 길이를 지정하며 버퍼 오버플로우를 예방하는데 있어 완전히 쓸모없다. 반대로 정확도 지정은 그 특정 문자열이 문자열 변환 지정자로 사용될 때 출력에서 가질 수 있는 최대 길이를 지정한다 - 따라서 버퍼 오버플로우에 대해 보호하기 위해 사용될 수 있다. 정확도 지정은 문자열을 다룰 때 단지 총 최대 길이를 지정함을 주목해라; 다른 변환 연산과는 다른 의미를 갖는다. 크기가 "*" 로 주어지면 최대 크기를 변수로서 넘겨줄 수 있다 (예, sizeof() 연산의 결과). 이는 예로서 가장 쉽게 보여진다 - 다음 버퍼 오버플로우에 대해 보호하는 틀린 그리고 옳은 방식이다:

 char buf[BUFFER_SIZE];
 sprintf(buf, "%*s",  sizeof(buf)-1, "long-string");  /* WRONG */
 sprintf(buf, "%.*s", sizeof(buf)-1, "long-string");  /* RIGHT */

이론상 sprintf() 는 복잡한 포맷을 지정하는데 사용할 수 있기 때문에 매우 도움이 되는 반면 슬프게도 sprintf() 를 틀리게 사용하는 것은 쉽다. 포맷이 복잡하다면 수신지가 전체 포맷의 가능한 최대 크기에 대해 충분히 크며 정확도 필드는 단지 한 변수의크기만 제어하는지를 확인할 필요가 있다. "가능한 가장 큰 (largest possible)" 값은 복잡한 출력이 생성되고 있을 때 결정하기 어렵다. 프로그램이 가능한 가장 긴 조합에 대해 충분한 공간을 할당하지 않는다면 버퍼 오버플로우 공격을 받기 쉬울 것이다. 또한 sprintf() 는 전체 연산이 끝난 후 수신지에 NUL 을 추가한다 - 이 여분의 문자는 잊기 쉬우며 이 문자 하나가 없음으로써 에러가 생길 수 있는 기회를 생성한다. 그래서 이것이 작동하더라도 어떤 상황에서 사용하는 것은 무척 곤란할 수 있다.

또한 위 코드에 대한 언급 - sizeof() 연산이 배열의 크기를 사용했음을 주목해라. 코드가 변경되어 "buf" 가 어떤 할당 메모리에 대한 포인터라면 모든 "sizeof()" 연산은 변경되어야 할 것이다 (또는 sizeof 는 단지 포인터의 크기를 측정할 것이며 이는 대부분의 값에 대해 충분한 공간이 아니다).

5.2.2. 정적 및 동적 할당 버퍼

strncpy 와 같은 함수들은 정적으로 할당된 버퍼를 다둘 때 유용한데 이는 버퍼가 "가장 긴 유용한 크기 (longest useful size)" 에 대해 할당되어 그때부터 고정된 크기로 머물러 있는 프로그래밍 접근 방법이다. 대안은 버퍼를 필요로 할때마다 그 크기를 동적으로 재할당하는 것이다. 두 접근 방법 모두 보안에 밀접하게 관련되어 있다.

고정된 길이의 버퍼를 사용할 때 일반적인 보안 문제가 있는데 버퍼가 고정된 길이를 갖는 다는 사실이 악용될 수 있다. 이는 strncpy(3) 및 strncat(3), snprintf(3), strlcpy(3), strlcat(3) 과 다른 그러한 함수들과 관련된 문제이다. 기본 개념은 공격자가 실제 긴 문자열을 설정해서 문자열의 끝이 잘릴 때 마지막 결과가 공격자가 원했던 것 (개발자가 의도했던 것 대신에) 일 것이다라는 것이다. 아마도 문자열은 여러 작은 조각으로부터 연결된다; 공격자가 전체 버퍼와 길이가 같은 첫 조각을 만들 수도 있으며 따라서 추후 모든 문자열을 연결하려는 시도는 무용지물이 된다. 다음 약간의 특정 예이다:

정적으로 할당된 버퍼를 사용할 때 소스 및 수신지 인수의 길이를 실제로 고려할 필요가 있다. 입력과 결과되는 중간 계산의 정상성 (sanity) 을 검사함으로써 또한 이를 다룰 수 있다.

다른 대안은 고정된 크기의 버퍼를 사용하는 대신 모든 문자열을 동적으로 재할당하는 것이다. 이러한 일반적인 접근 방법은 프로그램이 임의 크기의 입력을 다룰 수 있기 때문에 (메모리를 다 소비할 때까지) GNU 프로그래밍 지침에 의해 추천되고 있다. 물론 동적으로 할당된 문자열과 관련된 주요 문제는 메모리가 부족할 수도 있다는 것이다. 메모리는 버퍼 오버플로우에 대해 걱정했던 부분이라기 보다는 프로그램내의 어떤 다른 곳에서 소비될 수도 있다; 모든 메무리 할당이 실패할 것이다. 또한 동적 재할당은 메모리가 비효율적으로 할당되게 하기 때문에 기술적으로 프로그램이 쓸 수 있는 충분한 가상 메모리가 있다고 하더라도 메모리가 부족하게 되는 것은 전적으로 가능하다. 이외에도 메모리가 부족하기 전에 프로그램이 아마도 많은 가상 메모리를 사용할 것이다; 이는 "thrashing" 즉 컴퓨터가 모든 시간을 디스크와 메모리사이를 왕복하는데 (유용한 작업을 하는 대신) 소비하는 상황으로 끝날 것이다. 이는 서비스 부인 공격과 같은 효과를 가질 것이다. 입력 크기에 대한 어떤 합리적인 제한이 도움이 될 것이다. 일반적으로 프로그램은 동적으로 할당된 문자열을 사용하는 경우 메무리가 고갈될 때 안전하게 실패하도록 설계되어야 한다.

5.2.3. strlcpy and strlcat

OpenBSD 에 의해 사용된 대안은 Miller 와 de Raadt [Miller 1999] 에 의한 strlcpy(3) 과 strlcat(3) 이다. 이는 C 문자열 복사 및 연결에 다른 (에러가 덜 생기는) 인터페이스를 제공하는 최소한의 정적 크기를 갖는 버퍼 접근 방법이다. 이 함수의 소스와 문서는 ftp://ftp.openbsd.org/pub/OpenBSD/src/lib/libc/string/strlcpy.3 에서 새로운 BSD 스타일 오픈 소스 라이센스하에서 얻을 수 있다.

우선 다음은 원형 (prototype) 이다:

size_t strlcpy (char *dst, const char *src, size_t size);
size_t strlcat (char *dst, const char *src, size_t size);

두 함수 모두 인수로서 (복사되는 문자의 최대 수가 아닌) 수신지 버퍼의 절대 크키를 취하며 마지막을 종결 문자 NIL 로 채우는 것을 보장한다 (크기가 0보다 크다면). size 에 NIL 을 위한 바이트를 포함해야 함을 기억해라.

strlcpy 함수는 NUL 로 끝나는 문자열 소스를 NIL 로 끝나는 결과인 dst (destination) 로 size-1 문자까지 복사한다. strlcat 함수는 dst 끝에 NIL 로 끝나는 문자열 소스를 추가한다. 이는 기껏해야 size-strlen(dst)-1 바이트를 NIL 로 끝나는 결과에 추가할 것이다.

strlcpy(3) 과 strlcat(3) 의 한가지 중요하지 않은 단점은 대부분의 유닉스 계열 시스템에 디폴트로 설치되어 있지 않다는 것이다. OpenBSD 에는 <string.h> 내에 존재한다. 이는 그렇게 어려운 문제는 아니며 이들은 작은 함수이기 때문에 각자 프로그램 소스에 이들을 포함할 수 있으며 이들을 적재할 수 있는 별도의 작은 패키지를 만들 수도 있다. 이를 자동적으로 다루기 위해 autoconf 를 사용할 수도 있다. 더욱 많은 프로그램이 이들 함수를 사용한다면 오래지 않아 이들은 리눅스 배포판 및 다른 유닉스 시스템 계열의 표준 부분이 될 것이다. 또한 이 함수들은 glib 라이브러리에 최근에 추가되었으며 (필자는 이를 위해 패치를 제출하였다) 따라서 glib 를 사용함으로써 이들을 이용할 수 있을 것이다 (미래에). glib 에서는 glib 라이브러리 네이밍 관례에 일치하는 g_strlcpy 와 g_strlcat 로 이름지어졌다.

또한 strlcat(3) 은 제공된 크기가 0 이거나 또는 수신지 문자열 dst (주어진 문자내에서) 내에 NIL 문자가 없다면 약간 다른 의미를 갖는다. OpenBSD 에서는 크기가 0 이면 수신지 문자열의 길이는 0 으로 간주된다. 크기가 0 이 아니지만 수신지 문자열 (문자의 크기 수내에서) 내에 NIL 문자가 없다면 수신지의 길이가 크기와 같다고 간주한다. 이러한 규칙들은 내장 NIL 문자가 없는 문자열을 일관성있게 다룬다. 불행히 적어도 솔라리스는 원래 문서에 이러한 규칙들으 들을 구체적으로 명시하지 않았기 때문에 이들을 따르지 않는다. 저자는 Todd Miller 에게 말했으며 둘 모두 OpenBSD 의미 체계가 옳다는 것에 동의하였다 (솔라리스는 옳지 않다). 증명은 간단하다: 어떠한 조건하에서도 strlcat 또는 strlcpy 가 크기 범위밖에서 수신지 내의 문자들을 조사하지 않아야 한다; 이러한 접근은 범위가 초과된 메모리를 접근함으로써 코어 덤프를 야기할 수도 있으며 더 나아가서는 기억 장치 대응 입출력 (memory-mapped I/O) 를 통해 하드웨어 상호작용을 야기할 수도 있다. 따라서, 다음과 같은 경우:

  a = strlcat ("Y", "123", 0);

정확한 답은 3 (0+3=3) 이지만 솔라리스는 수신지내의 "크기" 길이를 넘어서는 문자를 부정확하게 보기때문에 답이 4 라고 주장할 것이다. 우선 저자는 크기가 0 이거나 수신지가 NIL 문자를 갖지 않는 경우를 피하라고 제안한다. glib 의 향후 버전에서는 이 차이가 없을 것이며 늘 OpenBSD 의 의미 체계를 사용할 것이다.

5.2.4. libmib

자동적으로 문자열을 동적으로 재할당하는 C 의 한가지 툴셋은 ``libmib 할당 문자열 함수" 이다. 이는 Forrest J. Cavalier III 에 의한 것으로 http://www.mibsoftware.com/libmib/astring 에서 얻을 수 있다. libmib 에는 두가지 변형이 있는데 ``libmib-open" 은 수정과 재배포를 허용하는 X11과 유사한 라이센스하의 오픈 소스처럼 보이지만 재배포시 다른 이름을 선택해야 한다. 그러나 개발자는 이것이 ``완벽히 검사되지 않았을 지도 모른다" 라고 말하고 있다. 계속해서 발전된 libmib 를 얻기 위해서는 가입을 위해 돈을 지불해야 한다. 문서는 오픈소스가 아니지만 자유로이 사용할 수 있다.

5.2.5. C++ std::string class

C++ 개발자는 내장되어 있는 std::string 클래스를 사용할 수 있다. 이는 스토리지가 필요한만큼 커지는 동적인 접근 방법이다. 그러나 이 클래스의 데이타가 버퍼 오버플로우 문제가 발생할 수 있는 ``char *" 로 소리없이 변환될 수 있음을 언급하는 것은 중요하다. 따라서 이 클래스로부터 변환할 때 주의할 필요가 있다.

5.2.6. Libsafe

루슨트 테크놀로지사의 Arash Baratloo, Timothy Tsai 와 Navjot Singh 는 stack smashing 공격에 취약하다고 알려진 몇가지 라이브러리 함수의 wrapper 인 Libsafe 를 개발했다. 이 wrapper (이들은 미들웨어라고 부른다) 는 strcpy(3) 과 같은 C 라이브러리 함수의 수정 버전을 포함한 간단한 동적 적재 라이브러리로 이 수정 버전은 원래 기능성을 구현하지만 모든 버퍼 오버플로우가 현재 스택 프레임내에 포함되어 있음을 보장하는 방식으로 구현되어 있다. 초기 성능 분석은 이 라이브러리의 오버헤드가 매우 작다고 제안한다. Libsafe 논문과 소스 코드는 http://www.bell-labs.com/org/11356/libsafe.html 에서 얻을 수 있다. Libsafe 소스코드는 완전한 오프 소스 LGPL 라이센스하에 얻을 수 있으며 많은 리눅스 배포업체들이 이를 사용하려고 하고 있다.

Libsafe 의 접근 방법은 어느 정도 유용한 것처럼 보인다. Libsafe 는 리눅스 배포업자에 의한 포함이 확실히 고려되고 있으며 또한 다른 점에 의해서도 고려할 가치가 있다. 예를 들어 저자는 맨드레이크 7.1 버전이 이를 포함함을 알고 있다. 그러나 소프트웨어 개발자로서 Libsafe 는 철저한 방어 (defense-in-depth) 를 지원하는 유용한 메카니즘이만 실제로 버퍼 오버플로우를 예방하지는 못한다. 코드 개발동안에 Libsafe 에만 의존하지 않아야 할 몇가지 이유가 다음에 설명된다:

Libsafe 개발자 자신은 소프트웨어 개발자가 libsafe 만 의존해서는 안된다라고 인정하고 있다. 다음은 이들의 말이다:

버퍼 오버플로우 공격에 대한 최상의 해결책은 결함있는 프로그램을 수정하는 것이라고 일반적으로 받아들여지고 있다. 그러나 결함있는 프로그램을 수정하는 것은 특별한 프로그램이 결함이 있다는 것을 알아야만 한다. libsafe 및 다른 대안 보안 조치를 사용함으로써 얻게 되는 이익은 아직까지 취약하다고 알려지지 않은 프로그램에 대한 미래의 공격에 대한 보호이다.

5.2.7. 다른 라이브러리

glibc 가 아닌 glib 라이브러리는 C 프로그래머에게 많은 유용한 함수를 제공하고 있는 널리 사용될 수 있는 오픈 소스 라이브러리이다. GTK+ 과 GNOME 모두 glib 를 사용한다. 앞에서 언급한 바와 같이 glib 버전 1.3.2 에 저자가 제출한 패치를 통해 g_strlcpy() 와 g_strlcat() 가 추가되었다. 이는 glib 의 나중 버전이 사용될 수 있기만 하다면 이러한 함수를 이식성있게 사용하는 것을 더욱 쉽게 한다. 현재 시점에서 저자는 glib 라이브러리 함수가 버퍼 오버플로우에 대해 보호함을 명백히 보이는 분석을 하지 않았다. 그러나 많은 glib 함수는 자동적으로 메모리를 할당하며 자동적으로 실패를 가로챌 어떠한 합당한 방식이 없이 실패한다 (예, 대신 다른 어떤 것을 하려고 함이 없이). 그 결과로 많은 경우에 있어 대부분의 glib 함수는 대부분의 보안적인 프로그램에 사용될 수 없다. GNOME 지침은 g_strdup_printf() 와 같은 함수 사용을 추천하는데 이는 메모리 고갈이 발생할 때 프로그램이 즉각적으로 크래쉬해도 무방하다면 좋다. 그러나 이를 받아들일 수 없다면 이런 루틴을 사용하는 것은 적절하지 못하다.