계: Input Validation and Representation
입력 검증 및 표현 문제는 메타 문자, 대체 인코딩 및 숫자 표현 때문에 발생합니다. 보안 문제는 입력을 신뢰하기 때문에 발생합니다. 문제로는 "Buffer Overflows", "Cross-Site Scripting" 공격, "SQL Injection", 그 외 여러 가지가 있습니다.
Buffer Overflow
Abstract
할당된 메모리 블록 경계 외부에 쓰면 데이터가 손상되거나 프로그램이 중단되거나 악성 코드가 실행될 수 있습니다.
Explanation
Buffer overflow는 가장 널리 알려진 형태의 소프트웨어 보안 취약점입니다. 대부분의 소프트웨어 개발자가 buffer overflow의 취약점이 무엇인지 알고 있지만 이전 응용 프로그램 및 새로 개발된 응용 프로그램 모두에 대한 buffer overflow 공격은 여전히 빈번하게 발생합니다. 문제의 일부는 Buffer overflow가 발생하는 다양한 방식 때문이고 일부는 이 취약점을 예방하는 데 사용하는, 오류가 발생하기 쉬운 기법 때문입니다.
전형적인 Buffer overflow 익스플로이트에서 공격자는 프로그램에 데이터를 보내고 프로그램은 크기가 작은 스택 버퍼에 데이터를 저장합니다. 그 결과, 함수의 반환 포인터를 비롯한 호출 스택에 있는 정보를 덮어씁니다. 데이터는 함수가 값을 반환할 때 공격자의 데이터에 들어 있는 악성 코드에 제어를 전달하도록 반환 포인터 값을 설정합니다.
이런 유형의 스택 buffer overflow가 일부 플랫폼 및 일부 개발 커뮤니티에서는 여전히 빈번하지만 대표적으로 힙 buffer overflow 및 off-by-one 오류를 포함하여 다른 유형의 buffer overflow 도 많이 있습니다. Building Secure Software[1], Writing Secure Code[2] 및 The Shellcoder's Handbook[3]을 비롯하여 buffer overflow 공격의 동작 원리를 자세하게 기술한 훌륭한 책들이 많이 있습니다.
코드 수준에서 buffer overflow의 취약점은 일반적으로 프로그래머의 가정 위반과 관련이 있습니다. C 및 C++의 많은 메모리 조작 함수는 범위 검사를 수행하지 않으며 자신이 동작을 수행하는 버퍼의 할당 범위를 쉽게 침범합니다.
Buffer overflow의 취약점은 일반적으로 다음 코드에 나타납니다.
- 외부 데이터를 사용하여 동작을 제어하는 코드.
- 코드 범위 밖에서 이행되는 데이터의 속성에 의존하는 코드.
- 너무 복잡해서 프로그래머가 정확하게 동작을 예측할 수 없는 코드.
다음 예제는 세 가지 시나리오를 모두 보여줍니다.
예제 1.a: 다음 샘플 코드는 코드가 외부 데이터를 사용하여 동작을 제어하는 첫 번째 시나리오에서 자주 발생하는 간단한 buffer overflow를 보여줍니다. 코드는
참조: 이런 유형의 buffer overflow 취약점(프로그램이 데이터를 읽고 나머지 데이터에 대한 이후의 메모리 작업에서 이 데이터의 값을 신뢰하는 경우)은 이미지, 오디오 및 기타 파일 처리 라이브러리에 자주 발생합니다.
예제 3: 다음은 코드가 로컬로 확인되지 않는 데이터의 속성에 의존하는 두 번째 시나리오의 예제입니다. 이 예제에서
코드는 변수 길이를 검사하기 때문에 범위 검사를 안전하게 수행하는 것처럼 보입니다. 코드는 나중에 이 변수 길이를 사용하여
이 예제의 코드가 이제까지 본 코드 중에 가장 복잡한 것은 아니지만 메모리 작업을 수행하는 코드에서 복잡성을 최소화해야 하는 이유를 잘 보여줍니다.
전형적인 Buffer overflow 익스플로이트에서 공격자는 프로그램에 데이터를 보내고 프로그램은 크기가 작은 스택 버퍼에 데이터를 저장합니다. 그 결과, 함수의 반환 포인터를 비롯한 호출 스택에 있는 정보를 덮어씁니다. 데이터는 함수가 값을 반환할 때 공격자의 데이터에 들어 있는 악성 코드에 제어를 전달하도록 반환 포인터 값을 설정합니다.
이런 유형의 스택 buffer overflow가 일부 플랫폼 및 일부 개발 커뮤니티에서는 여전히 빈번하지만 대표적으로 힙 buffer overflow 및 off-by-one 오류를 포함하여 다른 유형의 buffer overflow 도 많이 있습니다. Building Secure Software[1], Writing Secure Code[2] 및 The Shellcoder's Handbook[3]을 비롯하여 buffer overflow 공격의 동작 원리를 자세하게 기술한 훌륭한 책들이 많이 있습니다.
코드 수준에서 buffer overflow의 취약점은 일반적으로 프로그래머의 가정 위반과 관련이 있습니다. C 및 C++의 많은 메모리 조작 함수는 범위 검사를 수행하지 않으며 자신이 동작을 수행하는 버퍼의 할당 범위를 쉽게 침범합니다.
strncpy()
와 같은 범위 지정 함수도 잘못 사용되면 취약점을 일으킬 수 있습니다. 대부분의 buffer overflow의 원인은 메모리 조작과 데이터의 크기 또는 구성에 대한 가정 위반이 결합된 것입니다.Buffer overflow의 취약점은 일반적으로 다음 코드에 나타납니다.
- 외부 데이터를 사용하여 동작을 제어하는 코드.
- 코드 범위 밖에서 이행되는 데이터의 속성에 의존하는 코드.
- 너무 복잡해서 프로그래머가 정확하게 동작을 예측할 수 없는 코드.
다음 예제는 세 가지 시나리오를 모두 보여줍니다.
예제 1.a: 다음 샘플 코드는 코드가 외부 데이터를 사용하여 동작을 제어하는 첫 번째 시나리오에서 자주 발생하는 간단한 buffer overflow를 보여줍니다. 코드는
gets()
함수를 사용하여 임의의 양의 데이터를 스택 버퍼에 읽어들입니다. 이 함수가 읽는 데이터의 양을 제한할 방법이 없기 때문에 코드의 안전성은 사용자가 BUFSIZE
문자보다 적은 문자를 입력하는 것에 의존할 수 밖에 없습니다.예제 1.b: 이 예제는
...
char buf[BUFSIZE];
gets(buf);
...
>>
연산자를 사용하여 입력을 char[]
문자열에 읽어들임으로써 C++의 gets()
함수의 안전하지 않은 동작을 쉽게 모방할 수 있다는 것을 보여줍니다.예제 2: 또한 이 예제의 코드는 사용자 입력에 의존하여 동작을 제어하지만 범위 지정 메모리 복사 함수
...
char buf[BUFSIZE];
cin >> (buf);
...
memcpy()
를 사용하여 간접 참조를 추가합니다. 이 함수는 대상 버퍼, 소스 버퍼 및 복사할 바이트 수를 받습니다. 입력 버퍼는 범위 지정 read()
호출로 채워지지만 사용자는 memcpy()
가 복사하는 바이트 수를 지정합니다.
...
char buf[64], in[MAX_SIZE];
printf("Enter buffer contents:\n");
read(0, in, MAX_SIZE-1);
printf("Bytes to copy:\n");
scanf("%d", &bytes);
memcpy(buf, in, bytes);
...
참조: 이런 유형의 buffer overflow 취약점(프로그램이 데이터를 읽고 나머지 데이터에 대한 이후의 메모리 작업에서 이 데이터의 값을 신뢰하는 경우)은 이미지, 오디오 및 기타 파일 처리 라이브러리에 자주 발생합니다.
예제 3: 다음은 코드가 로컬로 확인되지 않는 데이터의 속성에 의존하는 두 번째 시나리오의 예제입니다. 이 예제에서
lccopy()
라는 함수는 문자열을 인수로 받아 모든 대문자를 소문자로 변환한 문자열의 힙 할당 복사본을 반환합니다. 이 함수는 str
이 항상 BUFSIZE
보다 작다고 예상하기 때문에 해당 입력에 대한 범위 검사를 수행하지 않습니다. 공격자가 lccopy()
를 호출하는 코드의 검사를 우회하거나 해당 코드를 변경하여 str
의 크기에 대한 가정을 참이 아닌 값으로 만드는 경우, lccopy()
는 strcpy()
를 범위 지정 없이 호출하여 buf
오버플로를 일으킵니다.예제 4: 세 번째 시나리오(코드가 너무 복잡하여 동작을 쉽게 예측할 수 없음)에 해당하는 코드가 아래에 나와 있습니다. 이 코드는 다양한 응용 프로그램에서 널리 사용되는 libPNG 이미지 디코더에서 발췌한 것입니다.
char *lccopy(const char *str) {
char buf[BUFSIZE];
char *p;
strcpy(buf, str);
for (p = buf; *p; p++) {
if (isupper(*p)) {
*p = tolower(*p);
}
}
return strdup(buf);
}
코드는 변수 길이를 검사하기 때문에 범위 검사를 안전하게 수행하는 것처럼 보입니다. 코드는 나중에 이 변수 길이를 사용하여
png_crc_read()
가 복사하는 데이터 양을 제어합니다. 하지만 길이를 테스트하기 직전에 코드는 png_ptr->mode
에 대한 검사를 수행하고 이 검사가 실패하면 경고를 표시하고 처리를 계속합니다. length
는 else if
블록에서 테스트되므로 첫 번째 검사가 실패하면 length
가 테스트되지 않고 png_crc_read()
호출에 무조건 사용되어 스택 Buffer Overflow를 허용할 수 있습니다.이 예제의 코드가 이제까지 본 코드 중에 가장 복잡한 것은 아니지만 메모리 작업을 수행하는 코드에서 복잡성을 최소화해야 하는 이유를 잘 보여줍니다.
예제 5: 이 예제는 프로그램의 복잡성 때문에 buffer overflow에 노출되는 세 번째 시나리오도 보여줍니다. 이 경우, 노출은 코드의 구조 때문이 아니라 함수의 모호한 인터페이스 때문입니다(이전 예제의 경우와 마찬가지).
if (!(png_ptr->mode & PNG_HAVE_PLTE)) {
/* Should be an error, but we can cope with it */
png_warning(png_ptr, "Missing PLTE before tRNS");
}
else if (length > (png_uint_32)png_ptr->num_palette) {
png_warning(png_ptr, "Incorrect tRNS chunk length");
png_crc_finish(png_ptr, length);
return;
}
...
png_crc_read(png_ptr, readbuf, (png_size_t)length);
getUserInfo()
함수는 지정한 사용자 이름을 멀티바이트 문자열 및 사용자 정보의 구조체에 대한 포인터로 받아 사용자에 대한 정보로 구조체를 채웁니다. Windows authentication이 사용자 이름에 유니코드를 사용하기 때문에 우선 username
인수를 멀티바이트 문자열에서 유니코드 문자열로 변환합니다. 그런 다음 이 함수는 unicodeUser
의 크기를 문자 수가 아닌 바이트 수로 잘못 전달합니다. 따라서 MultiByteToWideChar()
호출은 (UNLEN+1)*sizeof(WCHAR)
길이의 문자 또는(UNLEN+1)*sizeof(WCHAR)*sizeof(WCHAR)
바이트까지 (UNLEN+1)*sizeof(WCHAR)
바이트만 할당된 unicodeUser
배열에 쓸 수 있습니다. username
문자열에 UNLEN
문자 넘게 포함되면 MultiByteToWideChar()
호출은 버퍼 unicodeUser
오버플로를 일으킵니다.
void getUserInfo(char *username, struct _USER_INFO_2 info){
WCHAR unicodeUser[UNLEN+1];
MultiByteToWideChar(CP_ACP, 0, username, -1,
unicodeUser, sizeof(unicodeUser));
NetUserGetInfo(NULL, unicodeUser, 2, (LPBYTE *)&info);
}
References
[1] J. Viega, G. McGraw Building Secure Software Addison-Wesley
[2] M. Howard, D. LeBlanc Writing Secure Code, Second Edition Microsoft Press
[3] J. Koziol et al. The Shellcoder's Handbook: Discovering and Exploiting Security Holes John Wiley & Sons
[4] About Strsafe.h Microsoft
desc.dataflow.cpp.buffer_overflow