Ubuntu Linux 10.04.4 LTS에서 버퍼오퍼플로 취약점점검
정보보안기사 실기 준비를 염두에 두고 버퍼 오버플로우 공격 실제 실습에 관한 섹션을 재차 미루고 있었다. 만약 보안기사 실기에서 이 실습 과정의 원리를 캐묻는 문제가 출제될 가능성이 있었더라면 실행 과정의 캡처 화면을 업로드하면서 스스로 어떠한 논리로 공격이 이루어지는 것에 대한 개념을 되짚어보았을 것이다.
하지만 앞선 섹션에서 언급한 실제 공격을 위해서 알아야 할 어셈블리어 언어와 디버깅 단에서의 명령은 역어셈블리어(Disassembly), 역공학(Reversing)에서 판단할 수 있는 수준이므로 아직까지 실무형 문제로 정보보안기사에서 출제한 이력은 없었다. 역공학과 관련해서는 윈도 PE헤더의 종류를 묻는 문제 수준이었다. 그렇다고 이 실습의 의미가 없다는 것이 아니라, 공격의 원리에 대한 개념이 없다면 백날 실습해봐야 머리에 남는 지식도 없다는 것이다.
보안전문가로서 해킹을 하는 목적은 해킹을 시도하기 위해서가 아니라 해커가 어떤 취약점의 경로로 침투하느냐에 대한 필요한 제반 지식을 갖추기 위해서이다.
모든 지식의 효용성이 그렇듯이 자신이 아는 만큼 판단할 수 있고 활용할 수 있다. 실제 과정의 중요성을 무시하는 바는 아니지만 기본 개념에 대한 이론이 없다면 본인의 지식의 체에 걸러지는 알맹이 지식도 적을 수밖에 없다. 그런 까닭으로 리버스 엔지니어링의 기본 개념을 살필 수 있는 이 버퍼오버플로 공격을 진행하기 전에 섹션 1부터 이론에 대한 전체적인 감을 다시 잡고 시작하는 것이 나을 듯싶다.
일단 본인은 우분투 10.04.4 LTS (Lucid Lynx) 버전을 버추얼 박스(Virtual Box)를 통해 이미지(iso 파일)로 불러들여서 설치하였다. 그리고 터미널에서 다음과 같이 overflowtest.c라는 파일을 nano 툴(기본 탑재)을 통해 작성했다.
스택 기반 버퍼 오버플로우에 취약한 C 프로그램이다. 이 C 프로그램은 우선 stdio.h와 string.h 라이브러리를 포함하는 것으로 시작했는데, 이는 C에서 아무것도 없는 황무지에서 개간할 필요 없이 표준 입출력 및 문자열 생성자를 쓰게 해준다. 여기서는 프로그램 내에서 문자열과 콘솔로 문자를 출력하고자 한다(조지아 와이드먼, 침투 테스트).
그다음으로 세 개의 함수 overflowed(), function(), main() 이 등장한다. 만약 overflowed(공격이 성공하면 출력하는 소스 코드) 함수가 호출되면 콘솔에 "Execution Hijacked."라는 문자가 출력된 다음 main 함수로 복귀(Return)한다. function() 함수가 호출되면 지역 변수, 다섯 개의 문자를 담는 buffer라는 문자열을 선언한 다음 function 함수로 전달된 변수의 내용을 buffer로 복사한다. 프로그램이 시작될 때 기본적으로 호출(entry point)되는 시점인 main() 함수는 function() 함수를 부른 다음 프로그램이 수신한 첫 번째 커맨드 라인 인자(예를 들면 AAAA)를 전달한다. function() 함수가 리턴하면 main 함수는 콘솔에 "Executed Normally."라고 출력한 다음 프로그램이 끝난다.
정상적인 상황에서는 overflowed() 함수가 절대 호출되지 않기 때문에 "실행이 가로채어졌음(Execution Hijacked.)"이라는 문자열을 콘솔에서 볼 수 없다. 지금부터 프로그램 속의 공격을 위한 소스 코드로 가상하는 overflowed() 함수를 삽입하여 버퍼를 넘치게 함으로써 프로그램의 제어권을 가져오는 일련의 상황을 다루겠다.
다음과 같이 프로그램을 컴파일한다.
다음은 위의 명령어에 주어진 옵션에 대한 설명이다.
1) -g : GNU(GNU is Not Unix) 디버거인 GDB(GNU DeBuger)를 위해 디버그 정보를 추가하라는 뜻.
2) -fno-stack-protector : 이 플래그를 통해 GCC(GNU Compiler Collection) 스택 보호 메커니즘 해제.
3) -z execstack : 스택을 실행 가능하게 만듦으로써 다른 차원에서 버퍼 오버플로우를 방지하게 됨.
4) -o : overflowtest [실행 파일명]으로 컴파일함.
만약 기본적으로 패치가 되어 있는 리눅스 버전에서 위의 스택 보호 메커니즘을 해제하는 옵션 없이 컴파일하여 실행하면 아래와 같이 오버플로 방지가 이루어졌다는 결과를 확인할 수 있다.
그리고 아래와 같이 실행해보면 5 byte의 문자열(AAAA로 4 byte만 입력한 거 같지만 실제로는 뒤에 개행 문자로 공백 1 byte가 따라온다.)로 프로그램을 호출했을 때와 오지게 길게 해서 호출했을 때의 결과문이 다르다는 것을 알 수 있다.
비정상 종료(crash)된 프로그램의 문제점은 function() 함수에서 strcpy 함수를 써서 구현한 데서 발생한다. strcpy 함수는 문자열을 다른 곳에 복사하지만 제공된 인자가 목적지로 하는 문자열 변수의 범위에 들어맞는지 '경계를 확인'하지 않는다. 그렇기 때문에 strcpy 함수는 다섯 글자만 담는 목적지 변수에 네 개, 만약 다섯 개 이상의 심지어 수백 개의 글자를 복사하려는 시도에 취약점을 드러낼 수 있다. 다섯 칸(5 byte)을 담을 수 있는 문자열에 100개의 글자를 복사하면 나머지 95개의 문자는 스택의 인접한 메모리 주소를 덮어쓴다(조지아 와이드먼, 침투 테스트).
따라서 잠재적으로 function() 함수의 스택 프레임의 나머지와 더 높은 영역(high memory)의 메모리를 덮어쓴다. 그렇다면 스택 프레임 기저 바로 다음에 있는 메모리 주소를 기억할 수 있겠는가? 프레임 전의 값이 스택에 푸시되기 전에, main 함수(caller)는 function() 함수(callee)가 리턴했을 때 계속해서 실행할 주소를 스택에 푸시한다. 그런데 buffer에 복사할 문자열이 충분히 길다면 buffer로부터 EBP(Extended Base Pointer) 레지스터 레이블의 메모리 주소까지 계속 덮어쓰게 되어 리턴할 주소는 물론이고 main 함수의 스택 프레임까지 덮어쓰게 된다([AAAAA] 아래로 계속 덮어진 AAAA).
strcpy 함수가 function() 함수의 첫 번째 인자를 buffer로 위치시킨 다음 function() 함수는 main() 함수로 되돌아간다. 스택 프레임이 스택에서 팝(pop: push의 반대로 스택의 바깥으로 튀어나오면서 메모리에서 삭제되는 과정이므로 ‘clean up’이라고도 함.)하면 CPU는 리턴 주소의 메모리 위치에 있는 명령을 실행하려고 시도한다. 그런데 위의 스택 프레임과 같이 리턴 주소에 A를 여러 번 써서 덮어썼기 때문에 CPU는 메모리 주소 41414141(A를 네 번 쓴 문자열의 16진수 표현)에 있는 명령을 실행하려고 할 것이다(공격 가능성의 단초).
하지만 프로그램이 메모리의 아무 영역이나 읽고 쓰고 실행하면 극단적인 혼란이 발생하므로 그렇게 하지 못한다. 여기서 메모리 영역 '41414141'은 프로그램 영역 밖이므로 'Segmentation falut'가 나면서 프로그램의 실행이 멈춘다.
이제부터 프로그램의 실행이 좌초할 때 무슨 일이 벌어지는지 더 자세히 살펴보겠다. 다음에 소개할 GDB(GNU DeBuger)에 maintence info sections 명령으로 어떤 메모리 영역이 프로세스에 할당되었는지 확인할 수 있다(조지아 와이드먼, 침투 테스트).
우분투에는 GDB가 기본으로 설치되어 있으므로 여기서처럼 디버거에서 프로그램을 시작하고 다섯 글자가 최대인 버퍼에 넘치게 썼을 때 일어나는 일을 살펴보기로 한다.
위와 같이 GDB 내의 커맨드 라인에서 프로그램 소스 코드를 확인할 수 있는 것(list 1,16 명령)은 컴파일 시 걸었던 -g 플래그 때문이다. 프로그램을 실행하기 전에 프로그램의 특정 지점에 중단점(break point)을 걸어두고 해당 지점에서 메모리 상태를 관찰해본다.
위의 소스 코드에서 fuction() 함수를 호출하기 직전인 15행과 function() 함수 내부에서 strcpy 함수가 실행하기 직전인 10행과 실행한 뒤의 11행에 중단점을 break문을 통해 설정한다.
그리고 A를 네 번 써서 프로그램을 정상적으로 실행시켜 본다.
첫 번째 중단점은 main() 함수 내부의 function() 함수가 호출되기 바로 전이었므로 위의 화면처럼 프로그램 실행 후 15행에서 멈춘다. 그리고 x 명령으로 메모리 영역을 확인한다.
x/16wx $esp 명령은 ESP에서 시작하는 열여섯 개의 4 byte 워드를 16진수 형식으로 출력한다. ESP(Extended Stack Pointer)는 스택에서 가장 낮은 메모리 위치를 가리킨다고 했다. 첫 번째 fucntion() 함수가 호출되기 바로 전에 위치하므로 ESP는 main() 함수의 스택 프레임 꼭대기에 위치한다. 그런데 위의 화면 두 번째 라인에서 'Value can't be converted to integer.'라는 결과가 보인다. 컴퓨터가 64bit 일 경우는 레지스터명은 ESP나 EBP가 아니라 RSP나 RBP로 바뀐다. 그래서 명령어에서 esp를 'rsp'로 바꾸니 스택 메모리 내용의 확인이 가능하다.
왼쪽 끝열에 메모리 주소(레지스터 레이블)가 4 바이트씩 4개가 표시되고 해당 주소의 메모리 영역에 기록된 데이터들이 오른쪽에 4 바이트 단위로 4개씩 표시되었다. 이 경우 첫 번째 4 바이트(레지스터 레이블)는 ESP(끝단)에서부터 시작되어 그 아래 스택(EBP)에 이르는 값의 첫 4 바이트를 의미한다.
참조 서적 원문
Weidman, G. (2014). Penetration testing: A hands-on introduction to hacking. San Francisco: No Starch Press.