C 언어 공통 취약점
해당 글은 CERN Computer Security의 Common vulnerabilities guide for C programmers 글을 참고하여 작성하였습니다.
C언어에서 발생하는 대부분의 취약점은 버퍼 오버플로우와 문자열 처리 미흡과 관련되어 있다. 이는 segmentation fault를 유발하고 입력 값을 조작할 경우 임의 코드 실행으로 이어질 수 있다. 이에 대부분의 에러와 조치 방안을 살펴보자고 한다.
gets
stdio gets()
함수는 버퍼 길이를 검증하지 않아 사용 시 항상 취약성을 야기한다.
Vulnerable Code
#include<stdio.h>
int main() {
char username[8];
int allow = 0;
printf("Enter your username, please: ");
gets(username); //악의적인 값 삽입
if(grantAccess(username)) {
allow = 1;
}
if(allow !=0) { //username을 오버플로우하여 덮어씀
privilegeAction();
}
return 0;
}
Mitigation
fgets()
함수 사용 및 동적 메모리 할당
#include <stdio.h>
#include <stdlib.h>
#define LENGTH 8
int main () {
char* username, *nlptr;
int allow = 0;
username = malloc(LENGTH * sizeof(*username));
if (!username)
return EXIT_FAILURE;
printf("Enter your username, please: ");
fgets(username,LENGTH, stdin);
// fgets는 LENGTH-1이나 개행 문자 다음에 멈춘다.
nlptr = strchr(username, '\n');
if (nlptr) *nlptr = '\0';
if (grantAccess(username)) {
allow = 1;
}
if (allow != 0) {
priviledgedAction();
}
free(username);
return 0;
}
strcpy
strcpy()
내장 함수는 버퍼 길이를 검사하지 않아 목적지에 인접할 메모리영역을 덮어 쓸 수 있다. strcat()
및 strcmp()
함수 또한 취약한다.
Vulnerable Code
char str1[10];
char str2[]="abcdefghijklmn";
strcpy(str1,str2);
Mitigation
strlcpy()
함수를 사용하는 것이 가장 좋은 방법이다. BSD 시스템이 아닌 경우, 간단히 구현하여 조치할 수 있다.
#include <stdio.h>
#ifndef strlcpy
#define strlcpy(dst,src,sz) snprintf((dst), (sz), "%s", (src))
#endif
enum { BUFFER_SIZE = 10 };
int main() {
char dst[BUFFER_SIZE];
char src[] = "abcdefghijk";
int buffer_length = strlcpy(dst, src, BUFFER_SIZE);
if (buffer_length >= BUFFER_SIZE) {
printf("String too long: %d (%d expected)\n",
buffer_length, BUFFER_SIZE-1);
}
printf("String copied: %s\n", dst);
return 0;
}
또 다른 방법은 strncpy()
함수를 사용하는 것이지만 ‘\0’ 종료를 보장하지 않는다.
enum { BUFFER_SIZE = 10 };
char str1[BUFFER_SIZE];
char str2[]="abcdefghijklmn";
strncpy(str1,str2, BUFFER_SIZE); /* 복사할 문자 수 제한 */
//버퍼의 모든 문자가 '\0'으로 설정되게 하고 만약 소스 버퍼가 제한 길이보다 길면 복사본에 '\0'로 덮어쓴다.
if (str1[BUFFER_SIZE-1] != '\0') {
/* buffer was truncated, handle error? */
}
sprintf
이전 함수들과 마찬가지로 sprinf()
함수는 버퍼 경계를 검사하지 않아 오버플로우에 취약하다.
Vulnerable Code
#include <stdio.h>
#include <stdlib.h>
enum { BUFFER_SIZE = 10 };
int main() {
char buffer[BUFFER_SIZE];
int check = 0;
sprintf(buffer, "%s", "This string is too long!");
printf("check: %d", check); /* This will not print 0! */
return EXIT_SUCCESS;
}
Mitigation
snprintf()
함수를 사용하면 오버플로우 방지 및 최소 크기의 버퍼를 반환하는 장점이 있다.
#include <stdio.h>
#include <stdlib.h>
enum { BUFFER_SIZE = 10 };
int main() {
char buffer[BUFFER_SIZE];
int length = snprintf(buffer, BUFFER_SIZE, "%s%s", "long-name", "suffix");
if (length >= BUFFER_SIZE) {
/* handle string truncation! */
}
return EXIT_SUCCESS;
}
printf and friends
다른 취약점 분류로 string formatting attacks을 들 수 있다. 이는 정보 유출이나 메모리 덮어쓰기의 문제를 야기할 수 있다. 해당 에러는 printf()
, fprintf()
, sprintf()
, snprintf
등 포멧 스트링을 인자로 가지는 모든 함수에서 발생한다.
Vulnerable Code
#FormatString.c
#include <stdio.h>
int main(int argc, char **argv) {
char *secret = "This is a secret!\n";
printf(argv[1]);
return 0;
}
위 코드를 -mpreferred-stack-boundary=2
옵션을 설정하여 컴파일하면 흥미로운 결과를 확인할 수 있다.(64 비트의 경우 약간 다르게 동작하지만, 여전히 취약하다.)
$ gcc -mpreferred-stack-boundary=2 FormatString.c -o FormatString
$ ./FormatString %s
This is a secret!
$
-mpreferred-stack-boundary=2 옵션은 정보 누출의 원인이 될 수 없으며 설정하지 않아도 코드가 더 안전 해지지 않는다.
Mitigation
항상 포멧 스트링을 삽입하여 사용하며, 절대 사용자에게 곧바로 입력 받지 않는다.
File opening
파일을 열 때, 많은 문제가 야기될 수 있으므로 반드시 주의가 필요하다. (자세한 내용은 Kupsch and Miller가 작성한 튜토리얼 문서를 참고하자.) 파일 처리 시 공격 받을 수 있는 여러 가지 경우 중에서 두 개의 간단한 예를 살펴보자.
Symbolic link attack
파일을 생성하기 전에 파일이 존재하는 지 확인하는 것이 좋다. 그러나 공격자는 파일을 사용하는 순간 중요 파일에 대한 심볼릭 링크를 생성할 수 있다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define MY_TMP_FILE "/tmp/file.tmp"
int main(int argc, char* argv[])
{
FILE * f;
if (!access(MY_TMP_FILE, F_OK)) {
printf("File exists!\n");
return EXIT_FAILURE;
}
/* 이 시점에 공격자는 /etc/passwd에 대한 심볼릭 링크를 /tmp/file.tmp에 생성한다.
tmpFile = fopen(MY_TMP_FILE, "w");
if (tmpFile == NULL) {
return EXIT_FAILURE;
}
fputs("Some text...\n", tmpFile);
fclose(tmpFile);
/* 공격자는 성공적으로 /etc/passwd를 덮어쓴다. (root권한으로 구동된다는 가정) */
return EXIT_SUCCESS;
}
Mitigation
파일을 직접 접근하여 레이스 컨디션을 피하고 파일이 존재할 경우 덮어쓰지 않는다.
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#define MY_TMP_FILE "/tmp/file.tmp"
enum { FILE_MODE = 0600 };
int main(int argc, char* argv[])
{
int fd;
FILE* f;
/* 심볼릭 링크 제거 */
unlink(MY_TMP_FILE);
/* 이때, 심볼릭 링크를 복원한 경우 실패한다. - fopen(path, "w")을 보완한 형태 */
fd = open(MY_TMP_FILE, O_WRONLY|O_CREAT|O_EXCL, FILE_MODE);
if (fd == -1) {
perror("Failed to open the file");
return EXIT_FAILURE;
}
/* 파일 기술자 대신에 파일 포인터를 사용 */
f = fdopen(fd, "w");
if (f == NULL) {
perror("Failed to associate file descriptor with a stream");
return EXIT_FAILURE;
}
fprintf(f, "Hello, world\n");
fclose(f);
/* fclose()로 fd를 닫는다. */
return EXIT_SUCCESS;
}
O_WRONLY|O_CREAT|O_EXCL
O_WRONLY : 쓰기 전용으로 열기
O_CREAT : 해당 파일이 없으면 생성
O_EXCL : O_CREAT를 사용할 때, O_EXCL를 함께 사용하면, 이미 파일이 있을 때에는 open() 되지 않아 이전 파일을 보존
출처: falinux open 파일 열기