버퍼 오버플로/심화

덤프버전 : r20180326

파일:나무위키프로젝트.png
이 문서는 나무위키 컴퓨터 프로젝트에서 다루는 문서입니다.
해당 프로젝트 문서를 방문하여 도움이 필요한 문서에 기여하여 주세요!



1. 설명
2. 위 설명대로 했는데 안 되는데요?
3. 그래도 한번 시도는 해보고 싶은데요
4. return to Libc


버퍼 오버플로의 종류에는 스택이 있지만 여기서는 스택에 관하여 설명한다.
본 문서는 x86 어셈블리어C언어, 메모리구조에 대한 기초적인 지식이 있는 전공자를 대상으로 한 문서입니다.


1. 설명[편집]


우선 간단한 C언어 프로그램을 생각해보자.
#include #include void main(int argc, char** argv){	char buf[16];	strcpy(buf, argv[1]);	printf("%s\n", buf);}

단순히 실행시 인수로 받은 문자열을 버퍼에 옮기고 버퍼를 출력하는 코드이지만 여기에는 심각한 보안적 결함이 있다. 가령 ./a.out 1, ./a.out 12, ./a.out 123, 하는식으로 점차 그 인수를 늘려보자, 분명 버퍼는 16칸을 잡아놨지만 그걸 넘어서도 출력이 되고, 어느 순간부터는 에러를 뿜으며 프로그램이 중단된다. 이는 C언어가 문자열의 끝을 무조건 \0(NULL 문자)까지로 인식한다는점과, strcpy 함수가 얼마나 문자열을 버퍼로 복사시킬지를 즉 문자열의 길이를 제한하지 않는다는점에서 발생한 문제이다. 이렇게 넘처 흐른 버퍼가 왜 에러를 불러일으키는지 아래 코드를 보자.
0x0804849d <+0>:	push	ebp
	0x0804849e <+1>:	mov	ebp, esp
	0x080484a0 <+3>:	and	esp, 0xfffffff0
	0x080484a3 <+6>:	sub	esp, 0x30
	0x080484a6 <+9>:	mov	eax, DWORD PTR [ebp+0xc]
	0x080484a9 <+12>:	mov	DWORD PTR [esp+0xc], eax
	0x080484ad <+16>:	mov	eax, gs:0x14
	0x080484b3 <+22>:	mov	DWORD PTR [esp+0x2c], eax
	0x080484b7 <+26>:	xor	eax, eax
	0x080484b9 <+28>:	mov	eax, DWORD PTR [esp+0xc]
	0x080484bd <+32>:	add	eax, 0x4
	0x080484c0 <+35>:	mov	eax, DWORD PTR [eax]
	0x080484c2 <+37>:	mov	DWORD PTR [esp+0x4], eax
	0x080484c6 <+41>:	lea	eax, [esp+0x1c]
	0x080484ca <+45>:	mov	DWORD PTR [esp], eax
	0x080484cd <+48>:	call	0x8048360 <strcpy@plt>
	0x080484d2 <+53>:	lea	eax, [esp+0x1c]
	0x080484d6 <+57>:	mov	DWORD PTR [esp], eax
	0x080484d9 <+60>:	call	0x8048370 <puts@plt>
	0x080484de <+65>:	mov	eax, DWORD PTR [esp+0x2c]
	0x080484e2 <+69>:	xor	eax, DWORD PTR gs:0x14
	0x080484e9 <+76>:	je	0x80484f0 <main+83>
	0x080484eb <+78>:	call	0x8048350 <__stack_chk_fail@plt>
	0x080484f0 <+83>:	leave  
	0x080484f1 <+84>:	ret

C언어 코드로 제작된 실행파일의 main 함수를 gdb로 뜯어낸 상태이다. 지금 필요한 행만 뜯어서 보자.
0x0804849d <+0>:	push	 ebp
	0x0804849e <+1>:	mov	ebp, esp
	0x080484a3 <+6>:	sub	esp, 0x30
                                        (코드 실행중...)
	0x080484f0 <+83>:	leave  
	0x080484f1 <+84>:	ret

알다시피, 모든 프로그램 코드가 실행될 때 메모리 공간에 스택의 형태로 자리잡는다. 0번은 이러한 스택의 바닥이 될(여기서의 스택은 위에서 아래로 자란다) ebp를 먼저 스택에 삽입한다. 뒤이어 esp의 값을 ebp에 집어넣는데, 지금 상황에서는 ebp와 esp가 겹쳐져 있고, 현재 ebp의 값은 본 함수를 호출한 곳의 값이기 때문이다. 따라서 1번 과정을 통해서 ebp가 본격적으로 main함수의 코드를 담는 스택으로써의 베이스포인터 값을 할 수 있게 된다. 그리고 버퍼(본 코드에서는 16으로 잡았다)를 스택 안에 넣기 위해서 스택의 상위를 가리키는 esp값을 아래로 내려줄 필요가 있다. 6번의 코드가 그러하다. esp의 값을 0x30만큼 내림으로써 이제 그 안에서 버퍼라는 char형 배열이 존재할 수 있게 된다. 83번과 84번은 간단한데 이러한 방법으로 만들어진 스택을 해체하는 작업이다.

그런데 어디서 문제가 생기느냐, 바로 ebp위의 ret 값에서 문제가 생긴다. 모든 함수는 자신의 코드가 끝났을때(이러한 작업을 위 어셈블리코드에서 84번 함수가 진행한다) 다시 돌아가야 할 주소를 ebp에서부터 4바이트 위(ebp-4)에 저장한다. 우리는 앞선 예제에서 buf에 프로그래머가 예상한 값(16)보다도 많은 값을 넣을 수 있다는것을 알게 되었다.(strncpy가 아닌 strcpy로 썼다) 아까 흘러넘친 코드가 ret값을 채워버려 알 수 없는 미지의 세계로 프로그램 루틴을 점프시켰기 때문에 일어난 일이다. 당연히 단순한 프로그램이 자신의 영역이 아닌 메모리를 읽을 수는 없으므로 에러 당첨. 프로그램은 터지게 된다.

물론 단순히 프로그램 하나 다운시키는 것보다도 더 끔찍한 일은 발생할 수 있다. 가령 그것이 타인의 SetUID가 설정된(대표적으로 root)프로그램일때가 그러하다. 알다시피, SetUID가 설정된 프로그램을 실행하면 그 프로그램의 루틴동안에 한하여 EUID(일시적인 UID)가 SetUID를 설정한 유저의 것으로 변한다. 이제부터 저 위의 C코드를 root가 짜고 컴파일한 프로그램이라고 생각해보자. 그리고 단순히 ret주소를 오염시키는데에서 그치지 말고 bash의 실행주소를 끼워넣어보자.

적절한 bash의 주소를 적절히 ret값에 끼워넣었다면 그 프로그램은 bash를 실행시킬 것이다. 그리고 조용히 whoami를 쳐보자. root의 권한을 취득했다는 것을 알 수 있을 것이다이미 프롬프트 바뀐 걸로도 알 수 있겠지만 넘어가자

왜냐하면 모든 프로그램의 함수는 위 어셈블리어 코드의 84, 85번줄을 실행함으로써 정상적으로 끝이 난다(그중에서도 85번줄이 ret값의 주소를 실행시키는 것이다). 이렇게 끝이나면 SetUID의 임시 권한부여도 끝이 난다. 그렇기때문에 이러한 임시 권한부여를 끝내지 않고, 즉 프로그램을 끝내지 않고 그 진행을 탈취하여 프롬프트를 얻어낸다면? 아까 말했다시피 프로그램은 끝나지 않았으니 root 권한이다. 따라서 ret값을 실행시키므로써 끝나야될 SetUID 프로그램을 강제로 다른 프로그램(여기서는 bash)로 점프시켜버리므로써 제한된 권한을 충분히 활용할 수 있게 되는 것이다.


2. 위 설명대로 했는데 안 되는데요?[편집]


당연하다. 위 공격 이론은 1988년도부터 발명된 이론이다. 혹시 다음과 같은 에러가 뜨지는 않았는가?
*** stack smashing detected ***: ./a.out terminated
중지됨 (core dumped)
그렇다면 다시 위의 어셈블리어 코드 중 일부를 잠시 가져와보자.
0x080484ad <+16>:	mov	eax,gs:0x14
	0x080484b3 <+22>:	mov	DWORD PTR [esp+0x2c],eax
	0x080484b7 <+26>:	xor	eax,eax


	0x080484de <+65>:	mov	eax,DWORD PTR [esp+0x2c]
	0x080484e2 <+69>:	xor	eax,DWORD PTR gs:0x14
	0x080484e9 <+76>:	je	0x80484f0 <main+83>
	0x080484eb <+78>:	call	0x8048350 <__stack_chk_fail@plt>
	0x080484f0 <+83>:	leave

코드 실행에 앞서 gs:0x14에서 적절한 값을 업어와서 ebp와 다른 변수들이 자리잡는 공간(가령 앞선 buf 배열) 사이에 배치한다. 그리고 모든 코드의 실행이 끝날 무렵 다시금 배치해두었던 값과 다시 gs:0x14의 원본 값을 비교(xor) 해봐서 그 값이 오염되었으면 버퍼 오버플로가 발행한걸로 간주하고 프로그램 자체를 그자리에서 종료시켜버린다. 이 공격에서 가장 중요한 ret 구문을 실행조차 시키기 못하게 되는 것이다. 여기서 중간에 배치된 값을 canary 값이라고도 부르며 이를 Stack Smashing Protector라고도 부른다. 컴파일러에서 제공하는 방어책이다.

이것 말고도 아예 bash를 비롯한 프로그램의 주소값의 앞자리에 \00을 필수적으로 배치한다든지(앞서 말했듯 널이 검출되면 자동으로 거기까지가 문자열의 끝이다. 뒤에 뭘 써두던 배치되지 않는다) 아예 스택에서는 bash등의 프로그램이 실행되지 않게 한다든지[1] 또는 성공적으로 트리거해서 쉘을 땄음에도 불구하고 SELinux 로 인해 권한이 그대로 자신의 권한인 경우도 있는 등 방어책들이 무궁무진하고 뚫을 수 있는 방법도 무궁무진하다. 이러한 오버플로 공격과 방어의 발전과정은 보안이 창과 방패의 대결이라는 아주 단적인 증거이기도 하다.

카나리를 우회하는 방법에는 크게 두 가지가 있다. 첫 번째 방법은 그냥 카나리 값을 때려맞혀서 페이로드에 집어넣는 것이다. 무슨 소린가 싶겠지만 리눅스에서는 포크된 프로세스가 부모 프로세스와 카나리 값을 공유하기 때문에 가능한 것으로, 포크 기반으로 멀티스레딩을 구현하는 서버 프로그램을 공략할 때 많이 사용한다. 한 바이트씩 오버플로우시켜가면서 크래시가 나지 않는 값을 찾는 것을 워드 길이번 반복해 지금 돌고 있는 서버의 카나리 값을 찾아낼 수 있고, 이 이후로 값을 집어넣으면 카나리는 일단 넘어갈 수 있다. 단 이는 프로그램이 카나리에 무조건 들어가는 널 바이트(0x00)를 쓰고도 중지되지 않는 종류의 취약한 함수를 사용할 때만 쓸 수 있는 방법이다.

두 번째 방법은 아예 카나리를 한참 넘어서서 stack smashing detected를 띄우는 위 예외 처리기의 주소 자체를 바꿔버리는 것이다. 주로 윈도를 대상으로 하는 익스플로잇에서 이런 식으로 카나리를 뚫어버리는 공략법이 성행한다.

3. 그래도 한번 시도는 해보고 싶은데요[편집]


뭘 좋아할지 몰라서 예제를 준비했다.
#include #include void target(){	printf("OverFlow!\n");}int main(int argc, char** argv){	char buf[16];	strcpy(buf, argv[1]);	printf("%s\n", buf);}


이제 다음 코드를 gcc로 컴파일할 때 -fno-stack-protector 옵션을 맨 뒤에 삽입해보자. 위에서 나왔던 스택보호장치를 배제하는 명령이다. 준비물은 다음과 같다. vi, gcc, gdb. 기본적으로 리눅스에 깔려있는 것들이니 준비할 필요는 없고 gdb에 경우에는 intel문법을 쓰고 싶으면 set disassembly-flavor intel을 입력하면 된다.

그리고 gdb로 저 타겟의 주소를 구하고 프로그램을 실행할때 인수로 적당한 길이의 더미값 + 타겟의 주소를 주면 오버플로라는 문자열이 등장할 것이다. 인수 입력에는 Perl이나 Python과 같은 스크립트 언어를 추천한다.

자세한 원리 등은 이 문서를 참조하면 도움이 될 것이다.[2]


4. return to Libc[편집]


리턴 투 라이브러리 기법이란, RET를 쉘코드의 주소 등 프로그램 외부에 있는 주소로 덮어 씌우지 않고, 프로그램 자체의 함수 주소로 덮어 씌우는 방법이다. 주로 시스템함수들인 system, evev*(execl, execv, execle, execve, execlp, execvp) 함수가 쓰인다.
공격 방법은 버퍼 + SFP(4byte) + RET(4byte) + 4byte(함수 프롤로그 우회) + 인자의 주소 (4byte)로 쓰인다. 예를 들면, RET주소에 system함수의 주소를 넣고, 인자로 "/bin/sh"라는 문자열이 존재하는 주소를 넣으면 쉘이 실행된다.
자세한 사항은 TheLordOfTheBOF darknight->bugbear 문제 참고.


[1] DEP(Data Execution Prevention/데이터 실행 방지) 라고 하며 해당 영역의 실행권한을 제거한다.[2] 와우해커의 달고나 란 분이 과거에 작성하신 문서로 기초부터 아주 잘 설명되어 있다.