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가 작성한 튜토리얼 문서를 참고하자.) 파일 처리 시 공격 받을 수 있는 여러 가지 경우 중에서 두 개의 간단한 예를 살펴보자.

파일을 생성하기 전에 파일이 존재하는 지 확인하는 것이 좋다. 그러나 공격자는 파일을 사용하는 순간 중요 파일에 대한 심볼릭 링크를 생성할 수 있다.

#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 파일 열기

출처: Common vulnerabilities guide for C programmers

results matching ""

    No results matching ""