네덜란드 소프트웨어 기업인 ‘티오베(TIOBE)’에서는 매월 컴퓨터 프로그래밍 언어 순위를 티오베 지수로 발표합니다. 이번 6월에는 1위가 파이썬, 2위가 C++, 3위가 C로 C++가 역사상 처음으로 C언어를 능가해 C++ 언어의 인기를 실감할 수 있었는데요. 하지만 C와 C++ 언어는 버퍼 오버플로우와 같은 메모리 엑세스 관련 취약점이 발생할 수 있어 주의가 필요합니다. 이번 시간에는 1편에 이어 CWE를 활용해 C와 C++ 언어의 보안 약점 예시와 조치 방안을 소개해 드리겠습니다.
1. CWE-658/659: C/C++ 언어의 보안 약점
C++는 언어명에서도 느껴지듯이 C 언어를 발전시켜 만든 언어이기 때문에 C와 유사한 부분이 많습니다. C나 C++ 언어는 포인터를 선언해 메모리를 할당할 수 있어 메모리 공간을 효율적으로 사용할 수 있다는 장점이 있지만, 메모리 액세스 관련 소프트웨어 취약점으로 이어질 수 있습니다. 보안 취약점이 발생할 가능성도 높기 때문에 미국 백악관 국가사이버국장실에서는 C나 C++ 언어 대신 메모리 안전성이 확보된 프로그래밍 언어를 권장하기도 했는데요. 실제로 CWE 홈페이지에 들어가 CWE-659를 검색해보면 아래와 같은 취약점 목록을 확인할 수 있습니다. 그중 몇 가지 보안 약점을 소개하며 어떻게 취약점 발생을 방지할 수 있는지 설명해드리겠습니다.

✔️ 메모리 누수
메모리 누수(Memory Leak)는 프로그램이 동적으로 할당한 메모리를 해제하지 않아 사용하지 않는 메모리가 반환되지 않고 계속 차지하는 현상을 말합니다. 이는 주로 프로그래머가 메모리 할당 후 이를 해제하지 않거나, 메모리 해제 로직에 오류가 있을 때 발생합니다.
이 취약점은 시스템 메모리를 고갈 시켜 서비스 거부 공격을 유발하게 만들거나, 다른 보안 취약점과 결합되어 악용될 수 있습니다. 따라서 프로그래머는 동적 메모리 할당 후 반드시 적절한 해제를 수행하여 메모리 누수를 방지해야 합니다. 즉 할당된 메모리를 사용 한 후에는 반드시 메모리 해제 함수를 호출할 필요가 있습니다.
✔️ 버퍼 오버 플로우
버퍼 오버플로우(Buffer Overflow)는 컴퓨터 프로그램이 고정된 크기의 메모리 버퍼보다 더 많은 데이터를 쓰려고 할 때 발생하는 취약점 입니다. 이는 프로그램이 버퍼의 경계를 넘어 인접한 메모리 영역을 덮어쓰게 되어 예상치 못한 동작을 유발할 수 있습니다. 예를 들어 아래 코드에서 개발자의 실수로 인해 인자로 전달받은 size 값이 아니라 고정된 상수 값으로 buf의 사이즈를 할당한 후 복사연산을 수행한다면, src의 길이에 따라 잠재적으로 버퍼 오버플로우가 발생할 수 있습니다.
|
#define BUFSIZE 128
void someFunction(char* src, int size) {
char*buf = (char*)malloc(sizeof(char)*BUFSIZE);
strcpy(buf, src);
…
}
|
결과적으로 이 취약점은 임의의 코드를 실행하거나 메모리 데이터를 손상시켜 예기치 않은 동작을 발생하게 만드는 등 서비스 운영에 큰 지장을 초래할 수 있습니다. 이러한 문제를 방지하기 위해서는 버퍼의 크기를 초과하여 데이터를 쓰지 않도록 각별히 주의합니다. 이를 위해 입력 크기를 철저히 검증하고, 안전한 함수 사용을 권장합니다.
✔️ 정수 오버플로우
정수 오버플로우(Integer Overflow)는 프로그램에서 정수 타입 변수가 표현할 수 있는 최대값을 초과하거나 최소값보다 작은 값을 저장하려 할 때 발생하는 오류입니다. 예를 들어, 32비트 정수형 변수는 -2,147,483,648에서 2,147,483,647 사이의 값을 가질 수 있습니다. 만약 이 범위를 초과하는 연산이 수행되면 값이 순환하여 예기치 않은 결과를 초래합니다.
이 취약점은 버퍼 오버플로우를 유발시키거나, 권한 상승 공격, 데이터 무결성 손상 등 여러 방면에서 위험을 초래할 수 있습니다. 이와 같은 위험을 방지하기 위해, 개발자는 정수 연산 시 오버플로우가 발생하지 않도록 주의해야 합니다. 메모리 할당이나 프로그램의 로직상 중요한 연산을 수행하는 경우 반드시 사전에 값을 검증하는 로직을 사용해야 합니다.
✔️ 참조 회수의 부적절한 업데이트
C/C++에서 특정 리소스는 참조 카운트를 사용하여 관리를 수행하는 경우가 있습니다. 예를 들어 관리되는 포인터, 공유 메모리, 파일 디스크립터와 같은 리소스들이 이에 해당합니다. 이러한 리소스가 오류로 인해 너무 빨리 0에 도달하거나, 0에 도달하지 않는 경우, 리소스가 너무 빨리 해제되거나 해제되지 않는 문제를 야기할 수 있습니다.
따라서 이 취약점은 잠재적으로 메모리 누수 문제나 해제 후 사용과 같은 다른 취약점으로 이어집니다. 실제로, 이와 관련된 다수의 CVE 이슈가 보고 되었습니다. 이런 위험을 방지하기 위해서는 참조 카운트를 사용하는 알고리즘을 구현할 때 정확하게 참조 카운트가 수행될 수 있도록 구현을 할 필요가 있습니다.
✔️ 일치하지 않는 메모리 관리 루틴
일치하지 않는 메모리 관리 루틴은 주로 C와 C++의 동적 메모리 관리 방법을 혼용하여 쓸 때 발생하는 오류 입니다. 예를 들면 아래와 같습니다.
|
SomeClass* object = new SomeClass();
…
free(object);
|
C++의 new 연산자를 사용하여 할당한 객체를 C언어의 free 함수를 사용하여 메모리를 해제하는 경우가 있다고 봅시다. 이 경우 object 객체의 소멸자가 호출되지 않기 때문에 자원 누수, 혹은 메모리 손상 및 프로그램 충돌등의 문제가 발생할 수 있습니다. 이와 같은 위험을 방지하기 위해서는 짝이 되는 메모리 관리 방법을 잘 사용할 필요가 있습니다.
2. 안전한 SW 개발은 언어별 보안 약점을 이해하는 것!
CWE-658/659/660는 각각 C, C++, Java 언어로 작성된 소프트웨어에서 발생 가능한 취약점을 상세히 설명하고 있습니다. 해당 언어는 산업군을 막론하고 많이 활용되지만 특히, 국방에서도 많이 활용되는 언어인데요. 방위사업청에서는 ‘무기체계 소프트웨어 개발 및 관리 매뉴얼’에서 소프트웨어 결함을 방지하고자 CWE-658/659/660을 활용해 소스코드 취약점을 점검할 것을 요구하고 있습니다. 이번 편에서 다루지 않은 Java 취약점 관련 내용이 궁금하시다면 링크에서 확인 가능합니다.
위와 같이 1편과 2편을 통해서 Java, C, C++ 언어의 취약점을 소개해드렸습니다. 수많은 개발 언어가 존재하는 가운데, 안전하게 소프트웨어를 개발하기 위해서는 언어별 보안 약점을 이해하고 이에 대응하는 것이 핵심입니다. 각 언어의 특성에 맞는 보안 접근 방식을 적용함으로써 개발자들은 보다 견고하고 신뢰할 수 있는 시스템을 구축할 수 있기에, 개발자를 비롯한 보안 담당자들은 안전한 코딩 관행과 지속적인 보안 교육을 통해 언어에서 발생 가능한 취약점을 효과적으로 관리하고 예방해야 합니다.
스패로우는 소스코드 보안약점 점검 도구인 Sparrow SAST를 통해 CWE-658/659/660을 지원하고 있습니다. C와 C++, Java로 소프트웨어를 개발 중이라면 Sparrow SAST로 시큐어코딩을 적용해, 보다 안전하게 소프트웨어를 개발하세요!

