* 상위 문서 : [[버퍼 오버플로]] [include(틀:프로젝트 문서,프로젝트=나무위키 컴퓨터 프로젝트)] [목차] [[버퍼 오버플로]]의 종류에는 [[스택(자료구조)|스택]]과 [[힙]]이 있지만 여기서는 스택에 관하여 설명한다. 본 문서는 [[x86]] [[어셈블리어]]와 [[C(프로그래밍 언어)|C언어]], [[메모리]]구조에 대한 기초적인 지식이 있는 전공자를 대상으로 한 문서입니다. == 설명 == 우선 간단한 C언어 프로그램을 생각해보자. {{{#!html
#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 0x080484d2 <+53>: lea eax, [esp+0x1c] 0x080484d6 <+57>: mov DWORD PTR [esp], eax 0x080484d9 <+60>: call 0x8048370 0x080484de <+65>: mov eax, DWORD PTR [esp+0x2c] 0x080484e2 <+69>: xor eax, DWORD PTR gs:0x14 0x080484e9 <+76>: je 0x80484f0 0x080484eb <+78>: call 0x8048350 <__stack_chk_fail@plt> 0x080484f0 <+83>: leave 0x080484f1 <+84>: ret }}} 위 [[C(프로그래밍 언어)|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)로 점프시켜버리므로써 제한된 권한을 충분히 활용할 수 있게 되는 것이다. == 위 설명대로 했는데 안 되는데요? == 당연하다. 위 공격 이론은 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 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등의 프로그램이 실행되지 않게 한다든지[* DEP(Data Execution Prevention/데이터 실행 방지) 라고 하며 해당 영역의 실행권한을 제거한다.] 또는 성공적으로 트리거해서 쉘을 땄음에도 불구하고 SELinux 로 인해 권한이 그대로 자신의 권한인 경우도 있는 등 방어책들이 무궁무진하고 뚫을 수 있는 방법도 무궁무진하다. 이러한 오버플로 공격과 방어의 발전과정은 보안이 창과 방패의 대결이라는 아주 단적인 증거이기도 하다. 카나리를 우회하는 방법에는 크게 두 가지가 있다. 첫 번째 방법은 그냥 카나리 값을 때려맞혀서 페이로드에 집어넣는 것이다. 무슨 소린가 싶겠지만 [[리눅스]]에서는 포크된 프로세스가 부모 프로세스와 카나리 값을 공유하기 때문에 가능한 것으로, 포크 기반으로 멀티스레딩을 구현하는 서버 프로그램을 공략할 때 많이 사용한다. 한 바이트씩 오버플로우시켜가면서 크래시가 나지 않는 값을 찾는 것을 워드 길이번 반복해 지금 돌고 있는 서버의 카나리 값을 찾아낼 수 있고, 이 이후로 값을 집어넣으면 카나리는 일단 넘어갈 수 있다. 단 이는 프로그램이 카나리에 무조건 들어가는 널 바이트(0x00)를 쓰고도 중지되지 않는 종류의 취약한 함수를 사용할 때만 쓸 수 있는 방법이다. 두 번째 방법은 아예 카나리를 한참 넘어서서 stack smashing detected를 띄우는 위 예외 처리기의 주소 자체를 바꿔버리는 것이다. 주로 윈도를 대상으로 하는 익스플로잇에서 이런 식으로 카나리를 뚫어버리는 공략법이 성행한다. == 그래도 한번 시도는 해보고 싶은데요 == [[뭘 좋아하는지 몰라서|뭘 좋아할지 몰라서]] 예제를 준비했다. {{{#!html
#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]]과 같은 스크립트 언어를 추천한다. 자세한 원리 등은 [[http://www.hackerschool.org/HS_Boards/data/Lib_system/buffer_overflow_foundation_pub.pdf|이 문서]]를 참조하면 도움이 될 것이다.[* 와우해커의 달고나 란 분이 과거에 작성하신 문서로 기초부터 아주 잘 설명되어 있다.] == 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 문제 참고. [[분류:컴퓨터 보안]][[분류:해킹/기법]]