버퍼 오버플로

버퍼 오버플로(영어: buffer overflow) 또는 버퍼 오버런(buffer overrun)은 메모리를 다루는 데에 오류가 발생하여 잘못된 동작을 하는 프로그램 취약점이다. 컴퓨터 보안과 프로그래밍에서는 프로세스가 데이터를 버퍼에 저장할 때 프로그래머가 지정한 곳 바깥에 저장하는 것을 의미한다. 벗어난 데이터는 인접 메모리를 덮어 쓰게 되며 이때 다른 데이터가 포함되어 있을 수도 있는데, 손상을 받을 수 있는 데이터는 프로그램 변수와 프로그램 흐름 제어 데이터도 포함된다. 이로 인해 잘못된 프로그램 거동이 나타날 수 있으며, 메모리 접근 오류, 잘못된 결과, 프로그램 종료, 또는 시스템 보안 누설이 발생할 수 있다.
버퍼 오버플로가 코드를 실행시키도록 설계되거나 프로그램 작동을 변경시키도록 설계된 입력에 의해 촉발될 수 있다. 따라서 이는 많은 소프트웨어 취약점의 근간이 되며 악의적으로 이용될 수 있다. 경계 검사로 버퍼 오버플로를 방지할 수 있다.
버퍼 오버플로는 보통 데이터를 저장하는 과정에서 그 데이터를 저장할 메모리 위치가 유효한지를 검사하지 않아 발생한다. 이러한 경우 데이터가 담긴 위치 근처에 있는 값이 손상되고 그 손상이 프로그램 실행에 영향을 미칠 수도 있다. 특히, 악의적인 공격으로 인해 프로그램에 취약점이 발생할 수도 있다.
흔히 버퍼 오버플로와 관련되는 프로그래밍 언어는 C와 C++로, 어떤 영역의 메모리에서도 내장된 데이터 접근 또는 덮어쓰기 보호 기능을 제공하지 않으며 어떤 배열에 기록되는 데이터가 그 배열의 범위 안에 포함되는지 자동으로 검사하지 않는다.

기술적인 설명

버퍼 오버플로는 불충분한 경계 검사에 의해 버퍼에 쓰인 데이터가 버퍼에 이미 할당된 근접한 메모리 주소에 있는 데이터 값을 오염시킬 때 발생한다. 대부분 이는 문자열을 하나의 버퍼에서 다른 버퍼로 복사할 때 발생한다.

이용

버퍼 오버플로 취약점을 이용하는 기술은 구조, 운영 체제, 메모리 영역에 따라 다르다. 예를 들어 힙(heap, 동적 메모리 할당에 사용됨) 을 이용하는 방법은 함수 호출 스택을 이용하는 방법과 매우 다르다.

힙 기반 이용

힙 데이터 영역에서 일어나는 버퍼 오버플로를 힙 오버플로라 부르며 스택 기반 오버플로와는 다른 방법으로 이용할 수 있다. 힙 상의 메모리는 동적으로 응용 프로그램에 의해 실행시간 중에 할당되고 보통 프로그램 데이터를 보관하고 있다. 이용하는 방법은 이 데이터를 특정한 방법으로 오염시켜 응용 프로그램이 연결 리스트 포인터(linked list pointer)등과 같은 내부 자료 구조를 덮어쓰게 한다. 기본적인 힙 오버플로 기술은 동적 메모리 할당 연결(malloc 상위 수준 데이터)을 덮어씀으로써 프로그램 함수 포인터를 조작한다. 마이크로소프트 JPEG GDI+ 취약점은 힙 오버플로도 컴퓨터 사용자에 위험이 될 수 있다는 예이다.

이용당하는 것을 막는 장벽

읽히거나 실행되기 전에 버퍼를 조작하면 이용하려는 시도를 막을 수도 있다. 이러한 조작은 이용당할 위협을 완화시켜줄 수는 있지만 불가능하게 만들지는 못한다. 조작에는 대문자로 또는 소문자로 변환, 제어 부호 제거, 숫자 영문자 제거 등이 포함된다. 이러한 방지 수단을 회피하는 기술도 존재한다: 숫자 영문자 코드, 다형성 코드, 자체 수정 코드, libc 귀환 공격 등. 같은 방법을 침입 감지 시스템에 의한 피탐지를 회피하는 데도 사용할 수 있다. 어떤 경우에는, 유니코드 변환의 경우도 포함되지만, 서비스 거부만 가능한 것으로 취약점의 위협이 잘못 표현되었지만 사실은 임의 코드의 원격 실행이 가능할 수도 있다.

이용의 실제

실세계 이용에서는 다양한 이슈를 극복해야 공격을 신뢰성 있게 수행할 수 있다. 이러한 요소에는 주소에서의 무효값 바이트, 셸 코드 위치 가변성, 환경간 상이성, 그리고 다양한 대응수단이다.

레지스터에 저장된 주소로 건너뛰기

레지스터로 건너뛰기 jump to register 기법은 NOP-슬라이드와 같은 추가 공간이나 스택 위치 오프셋을 추정하지 않아도 잘 작동한다. 전략은 복귀 포인터를 덮어써서 통제된 버퍼, 즉 셸코드를 담고 있는 레지스터의 위치로 프로그램이 건너뛰게 만드는 것이다. 예를 들어 레지스터 A가 어떤 버퍼의 시작점을 담고 있다면, 이 레지스터를 오퍼랜드(operand)로 삼은 어떠한 건너뛰기 또는 호출로 실행 흐름의 제어권을 얻을 수 있다. 실전에서는 어떤 프로그램이 의도적으로는 어떤 특정 레지스터에 저장된 주소로 건너뛰는 명령을 포함하지 않을 수도 있다. 전통적인 해법은 어떤 의도되지 않은 적당한 작동코드 opcode 사례를 프로그램 메모리 어딘가 고정된 위치에서 찾는 것이다. 예를 들면, 뜻하지 않게 i386 JMP esp 명령이 포함된 경우가 ntdll.dll 안의 DbgPrint()함수에 있었다. 이 명령의 작동 코드는 FF E4이다. 이 연속 두 바이트 패턴이 call DbgPrint 명령에서 1Byte 떨어진 곳 주소 0x7C941EED에 나타났다. 공격자가 프로그램의 귀환 주소를 이 주소로 덮어 쓰면, 프로그램이 처음에는 0x7C941EED로 건너뛰어 실행 코드 FF E4를 JMP esp 명령으로 인식하여 스택의 정상 위치로 jump, 공격자의 코드를 실행할 것이다. 이 기법이 가능하다면 취약성의 심각성이 상당히 증가한다. 이는 왜냐하면 이용이 충분히 신뢰성 있게 작동하여 공격을 자동화, 실행하면 가상적으로 성공을 보증할 수 있게 되기 때문이다. 이러한 이유로, 이 기법이 인터넷 웜에서 가장 흔히 버퍼 오버플로 취약점을 이용하기 위해 사용된다. 이 방법은 또한 셸코드의 위치를 윈도 플랫폼 상에서 덮어씌어진 귀환 주소 뒤에 위치하더라고 가능하다. 실행 코드는 주소 0x00400000에 기반하고, x86은 리틀 엔디안 구조이므로 귀환 주소의 최후 바이트는 반드시 무효값이 되고, 이것이 버퍼 복사를 마감하며 그 이후에는 아무것도 씌지 않는다. 이로 하여 셸코드의 크기는 버퍼의 크기로 제한되며, 지나친 규제가 될 수 있다. 동적 연결 라이브러리 DLL은 고위 메모리 (0x01000000 이상)에 저장되며 따라서 무효 바이트가 없는 주소를 가지므로, 이 방법은 무효 바이트 (또는 다른 허용 안되는 문자)를 덮어씌어진 복귀 주소에서 제거할 수 있다. 이런 식으로 사용되면, 이 방법은 때때로 DLL 프램폴라이닝이라고 불린다.

보호 대응 수단

다양한 기법이 버퍼 오버플로를 감지 또는 방지하기 위하여, 다양한 대가를 지불하며 사용되어 왔다. 가장 신뢰성 있는 버퍼 오버플로를 회피 또는 방지하기 위한 방법은 언어 수준에서 자동 보호를 사용하는 것이다. 그러나 이러한 종류의 보호는 기존 코드에 적용할 수 없고, 때때로 기술적, 상업적, 또는 문화적 제한으로 취약한 언어를 요구한다.

프로그래밍 언어 선택

프로그래밍 언어 선택은 버퍼 오버플로 발생에 깊이 있는 영향을 미칠 수 있다. 2008년 기준으로, C와 그로부터 발전된 C++는 가장 인기 있는 언어들에 포함되며, 거대한 양의 소프트웨어가 이러한 언어로 작성되었다. C와 C++는 메모리상 어떤 부분에서도 데이터 접근과 덮어쓰기에 대한 내장 보호 기능이 없다. 더 구체적으로, 데이터가 어떤 배열 (버퍼의 구현)에 씌어지는 데이터가 그 배열의 범위 안에 씌어지는지 검사하지 않는다. 그러나 표준 C++ 라이브러리는 데이터를 안전히 저장하는 많은 방법을 제공하고, C언어에서 버퍼 오버플로를 회피하기 위한 기술도 존재한다. 장기간 작동시키며 안정성을 유지해야 하는 프로그램을 작성해야 한다면 보다 데이터 타입과 오버플로 검사가 엄격한 C#과 자바를 사용할 수 있다.
많은 다른 프로그래밍 언어는 실행시간 검사를 제공하고, 어떤 경우에는 심지어 컴파일 시 검사하여 C 또는 C++가 데이터를 덮어쓰고 계속하여 더 많은 명령을 실행하여 잘못된 결과가 프로그램을 깨뜨릴 수도 있는 경우 경고 또는 예외를 제기할 수도 있다. 그러한 언어의 예는 에이다, 리스프, 모듈라-2, 스몰토크, OCaml 그리고 C에서 발전된 사이클론과 D이다. 자바와 닷넷 바이트코드 환경도 경계 검사를 모든 배열에 대하여 요구한다. 거의 모든 인터프리터 언어는 잘 정의된 에러 조건에 신호를 보냄으로써 버퍼 오버플로에 대항 보호할 것이다. 어떤 언어가 충분한 형 정보(type information)를 제공하면 때때로 가능케 할것인지 불가능하게 할 것인지 정할 수 있게 해 준다. 정적 코드 분석은 다수의 동적 경계 검사를 제거할 수 있지만, 잘못 구현되거나 부적절한 케이스(case)는 심각하게 성능을 저하시킬 수 있다. 소프트웨어 공학자는 반드시 조심스럽게 안전성과 성능 비용 사이의 조율을 고려하여 언어와 컴파일러 설정을 결정하여야 한다.

안전한 라이브러리 사용

버퍼 오버플로의 문제는 C와 C++언어에서는 일반적이다. 왜냐하면 이들 언어가 데이터 형을 보관 장소로서 버퍼의 저수준 표현상 상세를 노출시키기 때문이다. 버퍼 오버플로는 반드시 따라서 버퍼 관리 코드안에서의 높은 수준의 정확성을 유지함으로써 회피되어야 한다. 오래전부터 표준 라이브러리 함수 중 경계를 검사하지 않는 함수의 사용을 피할 것이 권고되어 왔다: gets, scanf, strcpy 등. 모리스 웜은 핑거 데몬 안에서 호출되는 gets 함수를 이용하였다.
잘 씌어지고 검사된 추상 데이터형 라이브러리는 경계 검사를 포함한 버퍼 관리를 중앙집중화하고 자동적으로 실시하여 버퍼 오버플로 발생과 충격을 줄일 수 있다. 버퍼 오버플로가 일상적으로 일어나는 언어의 두가지 주요한 기본 데이터 형은 문자열과 배열이다; 따라서 버퍼 오버플로를 이러한 데이터 형에서 방지하는 라이브러리는 필요 영역 가운데 방대한 대다수를 담당해 줄 수 있다. 여전히 이러한 안전한 라이브러리를 사용하지 못한다면 버퍼 오버플로와 다른 취약점을 낳을 것이다; 자연스럽게 라이브러러 자체의 어떤 버그도 잠재적인 취약점이 된다. "안전한" 라이브러리 구현은 "개선된 문자열 라이브러리", Vstr, 어원을 포함한다. 오픈 BSD 운영체제의 C 라이브러리는 strlcpy와 strlcat 함수를 제공하지만 이들은 전 안전 라이브러리 구현보다 더 제한되어 있다.
2006년 9월 C 언어 표준 위원회 기술 보고서 24731이 공개되었다; 이는 표준 C 라이브러리의 문자열과 입출력 함수들에 기반하였으나 버퍼 크기 매개 변수를 추가로 가진 함수들을 명세하였다. 그러나 이런 함수들의 버퍼 오버플로 방지 효력은 논란의 여지가 있다. 프로그래머가 일부러 함수 호출 마다 개입하여야 하는데 이는 더 오래된 표준 라이브러리 함수의 버퍼 오버플로를 방지하게 만드는 것과 등가의 일이다.
가능한 더 안전한 기능을 제공하는 함수를 사용하는 것도 방법이다. 예를 들어 문자열의 길이를 구하는 C 함수 strlen, wcslen 함수는 NULL 문자를 만날 때까지 계속 다음 메모리를 참조해 나가는 메커니즘으로 되어 있어서 원래의 문자열 끝에 NULL이 없으면 최악의 경우 메모리 영역 끝을 넘어서까지 메모리를 참조하려 시도할 것이다. 이 때에는 좀 더 개선된 함수 strlen s나 wcslen s 를 사용하여 참조할 최대 메모리를 제한 할 수 있다. 이 외에 기존의 문자열 함수 뒤에 _s가 붙은 이름의 함수를 사용하면 버퍼 오버플로 문제를 개선할 수 있다.

버퍼 오버플로 보호

버퍼 오버플로 보호는 가장 일반적인 버퍼 오버플로를 검출하기 위해 사용되며 함수가 귀환할 때 스택이 변경되었는지 검사한다. 변경되었다면 프로그램이 세그멘테이션 오류를 발생 시키며 중단된다. 그러한 시스템의 세가지 예가 gcc 패치인 립세이프 LibSafe, 스택 가드, 프로폴리스이다. 마이크로소프트의 데이터 실행 방지 모드는 명백히 SEH 예외 처리기를 가리키는 포인터를 덮어쓰기로부터 보호한다. 더 강력한 스택 보호는 스택을 두가지로 나눔으로써 가능하다: 하나는 데이터용이고 다른 하나를 함수 귀환에 사용하는 것이다. 이러한 구분은 포스 프로그래밍 언어에 비록 안전을 위한 기능은 아니지만 채택되어 있다. 어쨌든, 이는 버퍼 오버플로에 관한 완벽한 해결책은 아닌 것이, 귀환 주소가 아닌 민감한 데이터는 여전히 덮어씌어질 가능성이 있다.

포인터 보호

버퍼 오버플로는 포인터를 조작함으로써 (저장되어 있는 주소를 포함하여) 작동한다. 포인트 가드는 컴파일러 확장으로 제안되었는데, 공격자가 신뢰성 있게 포인터와 주소를 조작하는 것을 방지한다. 접근 방법은 컴파일러가 추가 코드를 삽입하여 자동적으로 포인터를 사용 전후에 XOR-인코딩하도록 하는 것이다. 공격자가 (이론적으로는) 어떤 값이 포인터를 인코드/디코드할 때 사용할지 알지 못하기 때문에 어떤 값으로 덮어 써야 원하는 효과를 거둘 수 있을지 알기 힘들게 된다. 포인트가드가 배포된 적은 없지만 마이크로소프트에서 비슷한 접근 방법을 구현하여 윈도 XP SP2와 윈도 서버 2003 SP1 이후에 적용하였다. 포인터 보호를 자동 기능으로 구현하지는 않았고, API를 추가하여 프로그래머의 재량에 따라 호출하여 사용하도록 하였다. XOR이 선형이므로, 공격자가 암호화된 포인터를 조작하여 어떤 주소의 하위 바이트만 덮어쓸 수 있다. 이렇게 하면 공격자가 여러번 시도하거나 복수의 위치 (예를 들어 NOP 슬라이드 안의 어떤 위치) 에 대해 시도함으로써 공격이 성공할 수 있다. 마이크로소프트는 임의로 암호화 방안을 변경함으로써 이 약점을 보완, 부분적으로만 덮어쓰기가 가능하게 하였다.

주소 공간 배치 난수화

주소 공간 배치 난수화 (Address space layout randomization, ASLR)는 컴퓨터 보안 기능으로 중요 데이터 영역, 예를 들어 실행 코드의 기반 주소, 라이브러리, 힙, 스택 주소 등을 임의로 프로세서의 주소 공간에 배치하는 것이다. 함수와 변수를 찾을 수 있는 가상 메모리 주소의 난수화로 버퍼 오버플로 이용이 더 어려워지지만 불가능한 것은 아니다. 공격자가 이용 시도를 각각의 시스템에 따라 다르게 하도록 강요하므로 인터넷 웜 방어에 더 유용하다. 비슷하지만 덜 효과적인 방법은 프로세스와 라이브러리를 가상 주소 공간에 리베이스하는 것이다.

심층 패킷 조사

심층 패킷 조사로 네트워크 경계에서 아주 간단한 버퍼 오버플로 원격 시도를 공격 고유 신호과 경험적 방법으로 검출할 수 있다. 알려진 공격 고유 신호 또는 긴 NOP 명령이 검출되면 패킷을 막을 수 있으며, 공격 패킷 내용으 조금 달라도 사용할 수 있다. 패킷 스캐닝은 효과적이지 못한데, 그 까닭은 알려진 공격만 막을 수 있고 NOP 슬라이드는 다양한 방법으로 암호화 할 수 있기 때문이다. 공격자들은 경험론적 패킷 스캐너와 침입 감지 시스템의 탐지를 회피하기 위하여 영숫자, 탈바꿈, 자기 수정 셸코드를 사용하기 시작하였다.

이용의 역사

버퍼 오버플로는 1972년 컴퓨터 보안 기술 기획 연구에서 기법을 소개하였을 때부터 이해되었던 개념이다. "이 기능을 수행하는 코드는 출처와 목적지 주소를 적절히 검사하지 않으므로, 사용자가 모니터의 일부를 덮어쓰도록 허용한다. 이는 코드를 모니터에 삽입하여 제어권을 획득하는 데 사용될 수 있다." 오늘날 모니터는 커널이라고 불릴 것이다.
1980년대 개인용 컴퓨터의 확산으로 이 기법에 대해 아는 사람의 수가 증가하였다. 코모도어 PET 상에서 예를 들어 흔히 두 번째 테이프 버퍼를 사용하여 어셈블리 언어 루틴을 저장하였다. 어떤 프로그래머들은 최대 32KB인 컴퓨터의 주기억장소 공간에서 몇바이트를 절약하기 위해 베이직 언어의 번거로운 POKE 명령 사용 대신 프린트 버퍼의 시작점을 테이프 버퍼로 변경하여 6502 어셈블리 언어 코드(이상해 보이는 글자로 이루어진)를 직접 원하는 곳으로 출력하였다. 실제로는 프린터 버퍼가 테이프 버퍼 보다 길었기 때문에, 베이직 문자열이 쉽게 1024 바이트를 넘어갔고, PET상의 마이크로소프트 베이직 인터프리터와 간섭하였다. 초기 맥, 코모도어, 아타리등 초기 개인용 컴퓨터 부트 이미지 로더와 95, 98까지의 마이크로소프트 윈도 운영 체제는 버퍼 보호에 부적절하였고 많은 프로그래머들이 버퍼 오버플로에 대해 알게 되었다.
최초로 문서에 남은 적대적 버퍼 오버플로 이용은 1988년이었다. 모리스 웜이 인터넷에서 자신을 퍼뜨리기 위해 사용했던 몇가지 공격 가운데 하나였다. 그 프로그램이 이용했던 것은 핑거라는 유닉스 서비스였다. 1995년 토마스 로파틱은 독립적으로 버퍼 오버플로를 재발견하였고 자신의 발견을 버그트랙(Bugtraq) 보안 메일링 리스트에 공개하였다. 1년 후 1996년 엘리아스 레비 (알레프 원으로 알려진)는 프랙 지에 "재미와 소득을 위해 스택 때리기"라는 제목의 기사에서 스택 기반 버퍼 오버플로 취약점을 공략하는 단계별 소개를 공개하였다.
그 이후에 최소한 두가지의 대규모 인터넷 웜이 버퍼 오버플로를 이용하여 많은 수의 시스템에 영향을 주었다. 2001년 코드 레드 웜은 마이크로소프트의 인터넷 정보 서비스 5.0의 버퍼 오버플로를 이용했고, 2003년의 SQL 슬래머 웜은 마이크로소프트 SQL 서버 2000을 실행시키는 컴퓨터에 영향을 주었다.
2003년 허가된 엑스박스 게임 안에 존재했던 버퍼 오버플로가 이용되어 자작 게임을 포함한 허가 받지 않은 소프트웨어도 콘솔에서 모드칩이라고 알려진 하드웨어 변경 없이 이용할 수 있게 되었다. PS2 독립 공격도 버퍼 오버플로를 사용하여 같은 작용을 플레이스테이션 2에 일으켰다. 황홍 공격은 닌텐도 위에서 같은 효과를 거두었는데, 젤다의 전설 황혼의 공주의 버퍼 오버플로를 이용하였다.

예제 소스 코드

아래의 C 소스 코드는 일반적인 프로그래밍 실수를 보여준다. 일단 컴파일 되면, 너무 긴 커맨드라인 인자 문자열을 가지고 실행하면 프로그램은 버퍼 오버플로 오류를 발생할 것이다. 왜냐하면 이 인자의 길이를 체크하지 않고 버퍼를 채우는 데 사용하기 때문이다.
/* overflow.c - 버퍼 오버플로를 설명한다 */#include <stdio.h>#include <string.h>int main(int argc, char *argv[]){
  char buffer[10];
  if (argc < 2)
  {
    fprintf(stderr, "사용법: %s 문자열\n", argv[0]);
    return 1;
  }
  strcpy(buffer, argv[1]);
  return 0;}
9개 이하의 문자 스트링은 버퍼 오버플로를 일으키지 않는다. 10 이상의 문자들은 오버플로를 일으킨다. 이것은 언제나 부정확하지만 프로그램 오류나 세그먼트 폴트를 일으키지는 않는다. strncpy는 버퍼에 쓰일 문자의 개수를 제한할 수 있다.
이 프로그램은 strncpy를 이용해 아래와 같이 안전하게 다시 쓸 수 있다:
/* better.c - 문제 해결 방법 하나를 설명한다 */#include <stdio.h>#include <string.h>int main(int argc, char *argv[]){
  char buffer[10];
  if (argc < 2)
  {
    fprintf(stderr, "사용법: %s 문자열\n", argv[0]);
    return 1;
  }
  strncpy(buffer, argv[1], sizeof(buffer));
  buffer[sizeof(buffer) - 1] = '\0';  /* 문자열의 끝을 분명히 맺는다 */
  return 0;}