C언어 표준에 포함된 문자열 함수들 중에 일부는 매우 위험하다. 대표적인 함수가 strcpy와 sprintf함수다. 이 두 함수의 경우 출력 값으로 문자열 포인터를 전송한다. 하지만 출력 문자열 포인터의 크기를 입력 받지 않기 때문에 버퍼 오버런의 위험을 가지고 있다. 버퍼 오버런의 경우 보안상 취약점이 될 수 있다. 따라서 견고한 프로그램을 작성하기 위해서는 되도록 이 함수들을 사용하지 않는 것이 좋다.
버퍼 오버런이란 프로그램 내부에서 사용하는 메모리 공간을 프로그래머가 기존에 의도했던 것을 넘어서서 덮어 쓰는 것을 말한다. 스택 공간을 침범하는 것을 스택 오버런, 힙 공간을 침범하는 것을 힙 오버런이라 한다. 아래 코드는 어떻게 스택 오버런이 발생하는 지 보여준다.
TCHAR *src = TEXT("123456789");
TCHAR dest[5];
_tcscpy(dest, src);
위의 코드를 살펴보면 dest는 최대 4글자를 저장할 수 있다. 왜냐하면 C언어의 경우 끝을 알리기 위해서 NULL 종료 문자를 사용하기 때문이다. 하지만 실제로 복사를 하고자 하는 소스 문자열은 4글자보다 큰 문자다. 따라서 프로그래머가 잡아둔 메모리 공간을 침범해서 덮어쓰게 된다. 이렇게 될 경우 스택이 깨지고 코드가 엉뚱한 곳으로 리턴되는 결과를 만들 수 있다.
Windows에서는 이러한 보안상 취약한 함수들을 대체할 새로운 안전한 함수들을 작성해서 Platform SDK를 통해서 배포하고 있다. 이 함수들은 strsafe.h에 들어있다. 우선 간략하게 어떠한 함수들이 들어 있는지 살펴보도록 하자.
기존함수 | 대체 함수 |
strcpy | StringCbCopy, StringCbCopyEx StringCchCopy, StringCchCopyEx |
strncpy | StringCbCopyN, StringCbCopyNEx StringCchCopyN, StringCchCopyNEx |
strcat | StringCbCat, StringCbCatEx StringCchCat, StringCchCatEx |
strncat | StringCbCatN, StringCbCatNEx StringCchCatN, StringCchCatNEx |
sprintf | StringCbPrintf, StringCbPrintfEx StringCchPrintf, StringCchPrintfEx |
vsprintf | StringCbVPrintf, StringCbVPrintfEx StringCchVPrintf, StringCchVPrintfEx |
gets | StringCbGets, StringCbGetsEx StringCchGets, StringCchGetsEx |
strlen | StringCbLength StringCchLength |
함수 이름이 규칙적으로 지어진 덕분에 함수의 종류를 한눈에 파악할 수 있다. 전체적으로 을 네 가지 종류의 함수가 있다. Cb, Cch계열과 일반 함수와 Ex 함수가 그것이다. Cb계열의 함수는 버퍼 크기를 인자로 받는다. 즉, 버퍼가 몇 바이트 크기를 가지느냐 하는 것을 기준으로 삼는다. 반면에 Cch계열 함수들은 버퍼의 길이를 인자로 받는다. 몇 글자를 저장할 수 있느냐 하는 것을 기준으로 삼는다. Ex 함수는 일반 함수의 기능에 버퍼의 잘림과 패딩을 다루는 추가적인 기능을 가진 함수들이다.
일반 함수의 경우 표준 함수와 동일한 인자를 받도록 되어 있다. 단지 추가적으로 버퍼의 크기를 하나 더 받는다. 따라서 여기서는 StringCbCopy와 StringCchPrintf의 사용법만 살펴보도록 하겠다. 다른 함수들의 자세한 사용방법을 알고 싶다면 MSDN을 참고하도록 하자.
HRESULT StringCbCopy( LPTSTR pszDest, size_t cbDest, LPCTSTR pszSrc);
StringCbCopy 함수의 원형이다. 이 함수는 strcpy와 동일한 기능을 한다. pszDest에는 복사될 버퍼 포인터를, cbDest에는 pszDest의 크기를, 그리고 pszSrc에는 복사할 문자열 포인터를 넣어주면 된다. cbDest를 제외하면 strcpy와 동일한 의미의 인자가 순서대로 입력된다는 것을 알 수 있다. 결과 값은 함수의 성공 여부다. 성공한 경우 S_OK를 리턴 한다. 위에 나열된 모든 String계열 함수의 리턴 값은 HRESULT다. COM에 사용되는 것과 동일한 타입이기 때문에 FAILED, SUCCEEDED매크로를 사용하면 손쉽게 에러 여부를 체크할 수 있다. StringCbCopy함수를 사용해 간단한 문자열을 복사하는 과정은 아래와 같다.
TCHAR dest[6];
TCHAR *src = "Hello World!";
if(FAILED(StringCbCopy(dest, sizeof(dest), src))) printf(TEXT("실패\n"));
printf(dest);
위의 코드를 실행해 보면 왜 StringCbCopy가 안전한지를 알 수 있다. 위 프로그램을 실행하면 dest값으로 Hello가 출력된다. 왜냐하면 dest의 크기인 6이 StringCbCopy함수 내부로 들어갔기 때문에 거기까지만 복사가 진행된 것이다. 더 이상 복사할 경우 버퍼 오버런이 발생하기 때문이다.
HRESULT StringCchPrintf( LPTSTR pszDest, size_t cchDest, LPCTSTR pszFormat, ...);
StringCbPrintf 함수의 원형이다. 이 함수는 sprintf와 동일한 기능을 한다. pszDest에는 출력될 버퍼를, cchDest에는 pszDest에 저장할 수 있는 글자 수를, 끝으로 pszFormat에는 포맷 문자열을 넣으면 된다. 아래와 같이 사용할 수 있다.
TCHAR buffer[MAX_PATH];
StringCchPrintf(buffer, MAX_PATH, "%s", TEXT("Hello World"));
문자열을 다루는 일은 프로그래밍 과정에서 광범위 하게 사용된다. 일부 프로그램은 문자열 처리 과정이 프로그램의 전부이기도 하다. 이처럼 문자열 처리 작업은 많이 사용되는 만큼 가장 많은 버그와 보안 허점이 나오는 곳이기도 하다. 이러한 문제를 해결하는 가장 좋은 방법은 기존의 불완전한 함수들을 사용하지 않는 것이다. 이런 이유 때문에 strsafe.h를 프로젝트에 포함시키게 되면 표준 문자열 함수를 사용하는 부분에서는 deprecated 경고가 발생한다. 하지만 deprecated 경고를 강제로 무시하고 싶은 상황도 있다. 어쩔 수 없이 써야 하는 라이브러리 코드 등에서 표준 문자열 함수를 사용한 경우가 대표적이다. 이럴 때 경고를 강제로 끄기 위해서는 strsafe.h를 포함시키는 부분 앞에 STRSAFE_DEPRECATE를 정의해주면 된다. 아래와 같이 include를 시키면 표준 문자열 함수에 대한 경고가 발생하지 않는다.
#define STRSAFE_DEPRECATE
#include <strsafe.h>
String 계열의 함수가 안전하고 좋은 것임은 사실이다. 하지만 기존의 ANSI C/C++ 의 표준 문자열 함수들로 작성된 프로젝트를 String 계열의 함수로 교체하는 작업은 신중하게 결정해야 한다. 언뜻 보기에는 함수명을 바꾸는 간단한 작업처럼 보이지만 실상은 그렇지 않다. 기존 라이브러리 함수들을 사용하는 대부분의 코드의 경우 함수로 출력 버퍼의 크기를 전송하지 않기 때문에 호출하는 쪽과 함수 코드를 전체적으로 수정해야 한다. 이런 이유로 대부분의 경우 String계열로 코드를 고침으로써 얻는 보안 효과보다 더 많은 버그가 수정 도중에 발생한다. 따라서 기존의 프로젝트 코드를 변경하는 일은 신중히 검토한 후 결정하도록 하자.
말은 쓰는 사람의 혼을 담는 그릇이라고 한다. 이와 마찬가지로 코드는 프로그래머의 혼을 담는 그릇이 될 수 있다. 앞으로 새롭게 작성하는 프로젝트에는 안전한 문자열 함수를 쓰고 문자열 포인터가 전달되는 곳으로는 항상 크기를 같이 전달하도록 하자. 이보다 좀 더 좋은 방법은 되도록 직접적인 문자열 포인터의 사용을 줄이고 string이나 CString등의 C++ 클래스를 사용하는 것이다.