C++ 소프트웨어 보안 이슈들: 온라인 세미나 다시보기 & 요약 정리
- 2021-04-19
- Posted by: Narae Kim
- Categories: 기술자료, 메인 노출
소프트웨어 개발 세상은 늘 새로운 사이버 보안 위협과 마주하고 있습니다. 보안에 취약한 소프트웨어가 얼마나 위험한지, 그리고 그 결과의 얼마나 파장이 큰지 우리 모두 너무나 잘 알고 있습니다.
메리 캘리(Mary Kelly)가 진행했던 C++에서의 보안을 주제로 한 온라인 세미나를 확인해보세요. 보안 이슈가 발생하는 이유와 해소 방법들을 확인할 수 있습니다.
온라인 세미나 다시보기는 아래 링크를 클릭해서 다시 볼 수 있습니다:
이 글에서는 위 온라인 세미나에서 설명하는 C++에서 발생 가능한 보안 이슈들과 해소 방법들을 간단히 정리해보겠습니다.
버퍼 오버런(Buffer Overruns)
다음은 “초기 스타일” 버퍼 오버런(buffer overrun)의 일반적인 형태 예제입니다:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
void bufFunc (char *str) { char buffer[10]; strcpy (buffer, str); } int _tmain(int argc, _TCHAR* argv[]) { char string [16]; printf (“Input some data : ”); gets(string); bufFunc (string); printf (“Additional data here : ”); } |
버퍼 오버플로우(buffer overflows) 처리는 일반적으로 다음과 같은 방식으로 해결합니다:
- 코드에 대한 코멘트
- 표준 C++ 라이브러리와 STL 사용
- 경계(bounds)를 체크하는 라이브러리 활용
- 버퍼 오버런(buffer overrun) 또는 오버플로우(overflow)의 경우, 일반적으로 퍼지 테스트(fuzz testing)를 가장 많이 사용합니다. 퍼지 테스트는 반랜덤화(semi-randomized) 값을 생성해 입력값을 테스트하는 기술로, 애플리케이션 안정성과 성능 확인에 필요합니다. 퍼즈 라이브러리 중 하나인 libFuzzer를 소개한 적도 있죠.
포맷 스트링(Format String) 문제
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
void vulnerable () { char buffer[60]; if (fgets (buffer, sizeof (buffer), stdin) == NULL) return; printf(buffer); } void notVulnerable () { char buffer[60]; if (fgets (buffer, sizeof (buffer), stdin) == NULL) return; printf (“%s”, buffer); } |
추천 해결 방법:
- 포맷팅 함수에 사용자가 직접 입력한 포맷 스트링을 전달하지 않도록 설정하세요.
- 고정된 포맷 스트링 또는 신뢰할 수 있는 소스의 포맷 스트링을 사용하세요.
- 컴파일러 경고와 오류를 꼭 주시하세요.
- 포맷 스트링을 사용할 때는 printf(“%s”, user_input)을 활용하세요.
- 더 좋은 방법은, 되도록 printf 계열 함수를 사용하지 않고 스트림 처리(stream operation)를 하는 것입니다.
인티저 오버플로우 (Integer Overflows)
인티저 오버플로우는 작업 결과가 데이터 타입에 허용된 최대값보다 클 때 발생합니다. 충돌, 로직 오류, 권한 상승, 임의 코드 실행 문제를 일으킬 수 있습니다.
다음과 같은 방법으로 쉽게 해결할 수 있습니다:
- 코드를 이해하고 학습해보세요. 수학을 조금 활용해보세요!
- 메모리 할당과 배열 인덱스가 오버플로우 되지 않도록 모든 계산 부분을 확인해보세요.
- 배열 오프셋(offset)과 메모리 할당 사이즈는 unsigned 변수를 사용합니다.
- 컴파일러 경고를 절대 그냥 지나치지 마세요.
- size_t를 사용할 때 부호 문제나 잘림 현상이 없는지 확인하세요.
배열 생성과 삭제
프로그램에 new를 사용할 때는, 리스크가 발생하지 않도록 unmanaged 개체를 생성하고, 나중에 delete를 호출해야 할 수도 있습니다. 이는 C++의 잘못된 관행으로, new나 delete를 사용하지 않도록 하세요. 더 좋은 방법은 현대식 C++로 스마트 포인트와 표준 라이브러리 컨테이너 클래스를 사용하는 것입니다. 정확하게 단 하나의 delete와 새로운 new를 매치하기 훨씬 쉽습니다.
잘못된 카피(copy) 연산자와 할당 선언
C++에서 카피(copy) 연산자는 개체에 새로운 변수가 만들어졌을 때 호출되곤 합니다. 만약 카피 연산자를 만들지 않으, 컴파일러가 카피 연산자를 생성합니다. 정말 좋은 기능이죠! 그런데 연산자를 제대로 설정하지 않으면, 오류가 반복 발생합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class PrtHolder { public: PtrHolder(void* p) : m_ptr(p) { } ~PtrHolder() { delete m_ptr; } private: void* m_ptr; }; |
클래스로 리소스를 제어하고자 할 때는, 비공개 카피 연산자(Private copy constructor)와 할당 연산자를 구현없이도 선언할 수 있습니다. 비공개로 선언한 클래스에 외부 클래스가 이 중 하나를 호출하려고 하면, 비공개 메소드 호출에 대한 컴파일러 오류가 발생합니다. 내부적으로 이 중 하나를 호출하려고 하는 경우에는 링크 에러가 발생합니다.
포인터 초기화
1 2 3 4 5 6 7 |
Foo* pFoo; if (GetFooPtr ( &pFoo ) ) { // some code } // If pFoo is uninitialized, this is exploitable pFoo -> Release(); |
포인터 문제가 발생하지 않기 위해서는 다음의 방법을 사용해보세요. C++에서는 다음 방식을 사용합니다:
- 선언할 때 포인터를 초기화합니다 – 정말 쉬운 방법이죠. 전에 사용했던 포인터 값을 걱정할 필요없이 더욱 쉽게 디버깅 할 수 있는 정말 좋은 방법입니다.
- 사용 후 제로 포인터가 출력됩니다.
- 메모리 누수를 방지하려면, 동일한 추상화 수준에서 반환하고 힙(heap)에서 메모리를 할당하면 됩니다.
- 포인터가 범위 내에 있는 동안은 블록(block)을 힙(heap)으로 반환합니다.
- 포인터 타입이 일치하는지 확인합니다.
STL에 대한 이해 부족
C++ 표준을 숙지하는 것이 중요합니다. C++ 언어의 발전을 위해 규범을 생성하는 정말 멋진 전문가들이 있습니다. C++11 부터 C++ 코드 보안을 둘러싼 많은 위험 요소들을 방지할 수 있는 기능들이 더욱 좋아졌습니다. C++ STL이나 C++ 표준 라이브러리에 대한 더 많은 정보는 www.cplusplus.com 이나 cppreference.com 을 꼭 참고해보시기 바랍니다.
유용한 자료들
- Writing Secure Code, Second Edition by Michael Howard and David LeBlanc
- 24 Deadly Software Security Sins: Programming Flaws and How to Fix Them by Michael Howard, David LeBlanc, John Viega
- Software Security: Building Security In by Gary McGraw
- Effective C++: 55 Specific Ways to Improve Your Programs and Designs (3rd Edition) by Scott Meyers
- STL Tutorial and Reference Guide by David Musser
12.0 12.1 AI AWS C++ c++빌더 chatgpt DelphiCon ios rad서버 RAD스튜디오 UI UIUX UX uxsummit vcl 개발 개발사례 고객사례 기술레터 기술백서 데브옵스 데이터 데이터베이스 델파이 리눅스 마이그레이션 맥 머신러닝 모바일 새버전 샘플 세미나 안드로이드 웹 윈도우 인공지능 인터베이스 출시 커뮤니티에디션 코드 클라우드 파이썬 파이어몽키 현대화