티스토리 뷰
















윈도우 버퍼 오버플로우 익스플로잇 개발 - 2  
 
 





스택 기반 버퍼 오버플로우가 발생하는 경우
스 택 기반 버퍼 오버플로우는 스택상의 어떠한 데이터 구조체의 데이터의 바운더리(boundary)를 넘어서 데이터를 저장하면서 스택을 조작할 수 있게 되는 현상을 말한다. 스택 기반 버퍼 오버플로우는 결국 프로그래머의 실수로 인한 프로그램 문제라고 할 수 있다. 이러한 버퍼 오버플로우는 주로 배열에 대해 strcpy와 같은 함수를 사용할 때 많이 발생한다. 예를 들어 다음과 같이 간단한 프로그램을 짠다.

#include

void function(char *str)
{
char buffer[10];
printf(“buffer original len:%d ”,sizeof(buffer));
strcpy(buffer,str);
printf(“buffer:%s ”,buffer);
printf(“buffer changed to:%d ”,strlen(buffer));
}

void main()
{
char *str=”We are the hackers of the world”;
printf(“str:%s len:%d ”,str,strlen(str));
function(str);
}

컴파일(gcc -o sbof_ex1 sbof_ex1.c)하고 실행하면 다음과 같은 결과를 얻을 수 있다.

str:We are the hackers of the world
len:31
buffer original len:10
buffer:We are the hackers of the world
buffer changed to:31
Segmentation fault

buffer 의 길이가 처음에 10이었다가 31바이트의 문자열을 복사하고 나서는 31로 변함을 알 수 있다. buffer는 배열이므로 동적으로 할당되지 않고 메모리의 스택에 위치하게 되는데, 31-10=21만큼의 데이터는 buffer가 아닌 스택의 다른 부분을 겹쳐 쓴 것이다. 스택 프레임의 구성상 이 부분은 함수의 리턴 주소를 포함하는 경우가 많은데 이 경우 마지막 라인과 같이 세그먼트 폴트 등의 치명적인 에러가 발생한다. 이러한 형태로 스택의 할당 버퍼 이상의 영역에 데이터를 복사하다가 스택의 다른 영역을 건드리는 것을 스택 기반 버퍼 오버플로우라고 한다. 이 프로그램을 디버그해 보면 다음과 같다.

bash-2.03# gdb ./sbof_ex1
GNU gdb 4.18
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type “show copying” to see the conditions.
There is absolutely no warranty for GDB. Type “show warranty” for details.
This GDB was configured as “i686-pc-linux-gnu”...

① function에 브레이크 포인트를 잡고 실행한다.

(gdb) break function
Breakpoint 1 at 0x804845a

(gdb) run
Starting program: /root/work/projects/sec_doc/attack/programming_errors/sbo/./sbof_ex1
warning: Unable to find dynamic linker breakpoint function.
GDB will be unable to debug shared library initializers
and track explicitly loaded dynamic code.
str:We are the hackers of the world
len:31

Breakpoint 1, 0x804845a in function ()

② 먼저 bt(back trace)를 해본다. 스택에서 현재까지의 함수 콜 스택을 볼 수 있다.

(gdb) bt
#0 0x804845a in function ()
#1 0x804850f in main ()
#2 0x400312e7 in __libc_start_main () from /lib/libc.so.6

③ EBP(Frame Pointer : 0xbfffec78)와 ESP(Stack Pointer : 0xbfffec60) 레지스터의 값을 확인한다. EBP와 ESP 사이에는 24바이트의 차이가 존재한다. 그 사이에는 로컬 변수와 기타 프로그램 로딩과 관련된 정보가 저장된다.
(gdb) info reg ebp esp
ebp 0xbfffec78 0xbfffec78
esp 0xbfffec60 0xbfffec60

④ EBP를 덤프하면 다음과 같다.

(gdb) x/10a $ebp
0xbfffec78: 0xbfffeca8 0x804850f 0x80485e0 <_IO_stdin_used+92> 0x80485e0 <_IO_stdin_used+92>
0xbfffec88: 0x1f 0x8048441 0x8049630 <_GLOBAL_OFFSET_TABLE_> 0x80496f8

0xbfffec98: 0x400fc974 <__check_rhosts_file+1580> 0x40009f00 <_dl_init_next+204>

⑤ EBP의 첫 번째 4바이트 값은 SFP(Saved Frame Pointer)로 이전 프레임의 프레임 포인터를 가리킨다.

(gdb) x/a $ebp
0xbfffec78: 0xbfffeca8

다음과 같이 SFP의 값의 내용을 다시 추적해 보면 결국 0x0으로 끝난다. 각각의 주소가 프레임들을 구별하는 부분이 된다. 0xbfffecc8이 첫 번째 프레임의 SFP임을 알 수 있다.

(gdb) x/a 0xbfffec78
0xbfffec78: 0xbfffeca8
(gdb) x/a 0xbfffeca8
0xbfffeca8: 0xbfffecc8
(gdb) x/a 0xbfffecc8
0xbfffecc8: 0x0

⑥ EBP의 두 번째 4바이트 값은 리턴 주소로 return이 불릴 때 실행할 명령이 들어 있는 텍스트의 주소를 의미한다.

(gdb) x/a $ebp+4
0xbfffec7c: 0x804850f

function 함수에서 리턴할 때 main+63 위치로 돌아간다는 것을 의미한다.
⑦ EBP의 세 번째 4바이트 값은 함수와 함께 전달된 인자로서 지시하는 곳을 덤프해 보면 인자로 넘겨진 스트링이 있음을 알 수 있다.

(gdb) x/a $ebp+8
0xbfffec80: 0x80485e0 <_IO_stdin_used+92>
(gdb) x/s 0x80485e0
0x80485e0 <_IO_stdin_used+92>: “We are the hackers of the world”
(gdb)

⑧ ESP의 내용을 확인하면 다음과 같다.

(gdb) x/6a $esp
0xbfffec60: 0xbfffec78 0x400649cc
0x400fb140 <_IO_2_1_stdout_> 0x8048600 <_IO_stdin_used+124>
0xbfffec70: 0xbfffec8c 0x400fc974 <__check_rhosts_file+1580>

(gdb) x/24x $esp
0xbfffec60: 0x78 0xec 0xff 0xbf 0xcc 0x49 0x06 0x40
0xbfffec68: 0x40 0xb1 0x0f 0x40 0x00 0x86 0x04 0x08
0xbfffec70: 0x8c 0xec 0xff 0xbf 0x74 0xc9 0x0f 0x40

⑨ nexti 등을 써서 명령을 실행한다. 파일 이름과 라인 번호로 브레이크 포인트를 잡아도 된다.

(gdb) nexti
buffer original len:10
0x804847d in function ()

⑩ strcpy가 실행된 이후에 다음과 같은 메시지를 보면 nexti를 멈춘다.

(gdb) nexti
buffer:We are the hackers of the world

⑪ ESP의 내용을 확인하면 다음과 같다.

(gdb) x/6a $esp
0xbfffec60: 0xbfffec78 0x400649cc
0x400fb140 <_IO_2_1_stdout_> 0x61206557
0xbfffec70: 0x74206572 0x68206568

(gdb) x/24x $esp
0xbfffec60: 0xbfffec78 0x400649cc 0x400fb140 0x61206557
0xbfffec70: 0x74206572 0x68206568 0x656b6361 0x6f207372
0xbfffec80: 0x68742066 0x6f772065 0x00646c72 0x08048441
0xbfffec90: 0x08049630 0x080496f8 0x400fc974 0x40009f00
0xbfffeca0: 0xbfffecf4 0x080485e0 0xbfffecc8 0x400312e7
0xbfffecb0: 0x00000001 0xbfffecf4 0xbfffecfc 0x40012584

(gdb) x/24c $esp
0xbfffec60: 120 ‘x’ -20 ‘疵 -1 ‘’ -65 ‘썬 -52 ‘庫 73 ‘I’ 6 ‘06’ 64 ‘@’
0xbfffec68: 64 ‘@’ -79 ‘’e 15 ‘17’ 64 ‘@’ 87 ‘W’ 101 ‘e’ 32 ‘ ‘ 97 ‘a’
0xbfffec70: 114 ‘r’ 101 ‘e’ 32 ‘ ‘ 116 ‘t’ 104 ‘h’ 101 ‘e’ 32 ‘ ‘ 104 ‘h’

⑫ ESP+0xc의 내용을 확인하면 다음과 같다. 결과적으로 buffer라는 배열은 ESP+0xc(0xbfffec6c), 즉 EBP-12 위치에서 시작했음을 알 수 있다. 즉, 12바이트가 buffer라는 배열에 할당됐는데 이것은 메모리가 4바이트 단위로 정렬되기 때문이다. ESP에서 ESP+0xc까지의 12바이트는 동적 로딩과 관련되는 값들인 것으로 추정된다.

(gdb) x/31c 0xbfffec6c
0xbfffec6c: 87 ‘W’ 101 ‘e’ 32 ‘ ‘ 97 ‘a’ 114 ‘r’ 101 ‘e’ 32 ‘ ‘ 116 ‘t’
0xbfffec74: 104 ‘h’ 101 ‘e’ 32 ‘ ‘ 104 ‘h’ 97 ‘a’ 99 ‘c’ 107 ‘k’ 101 ‘e’
0xbfffec7c: 114 ‘r’ 115 ‘s’ 32 ‘ ‘ 111 ‘o’ 102 ‘f’ 32 ‘ ‘ 116 ‘t’ 104 ‘h’
0xbfffec84: 101 ‘e’ 32 ‘ ‘ 119 ‘w’ 111 ‘o’ 114 ‘r’ 108 ‘l’ 100 ‘d’

⑬ function 함수의 리턴 주소가 저장되어 있던 스택의 내용을 덤프하면 다음과 같다.

(gdb) x/a $ebp+4
0xbfffec7c: 0x6f207372

(gdb) x/10c $ebp+4
0xbfffec7c: 114 ‘r’ 115 ‘s’ 32 ‘ ‘ 111 ‘o’ 102 ‘f’ 32 ‘ ‘ 116 ‘t’ 104 ‘h’
0xbfffec84: 101 ‘e’ 32 ‘ ‘

결국 0x6f207372라는 엉뚱한 주소로 프로그램은 리턴하게 되고 그곳의 코드를 실행하려 할 것이다.

⑭ 이곳은 아무런 함수도 정의되어 있지 않는 영역이다.

(gdb) disassemble 0x6f207372
No function contains specified address.

⑮ 결국 다음과 같이 세그먼트 폴트가 생긴다.

(gdb) cont
Continuing.
buffer changed to:31

Program received signal SIGSEGV, Segmentation fault.
0x6f207372 in ?? ()

만 약 SFP와 리턴 주소를 보전한다면 프로그램이 세그먼트 폴트되는 일은 없을 것이다. 다음 프로그램은 SFP와 리턴 주소를 임시 버퍼에 저장했다가 다시 복원하는 프로그램이다. 이 프로그램은 정상으로 종료됐다. 이와 같은 과정을 거쳐 OFFSET 값을 얻을 수 있는데 스트링 시작 부분과 EBP의 시작 부분의 차이가 OFFSET이 된다. OFFSET 값은 시스템에 따라 차이가 날 수 있다.

#include

void function(char *str)
{
char buffer[10];
char save_buffer[8];

#define OFFSET 12
printf(“buffer original len:%d ”,sizeof(buffer));
/* SFP와 RET 주소를 save_buffer에 저장한다. */
memcpy(save_buffer,buffer+OFFSET,8);
printf(“return address is 0x%.2x%.2x%.2x%.2x ”,save_buffer[4]&0xff,save_buffer[5] &0xff,save_buffer[6]&0xff,save_buffer[7]&0xff);
strcpy(buffer,str); /* 스택 기반 버퍼 오버플로우가 발생한다. */
/* 저장된 스택을 복원해 SFP와 RET 주소를 원상 복귀한다. */
memcpy(buffer+OFFSET,save_buffer,8);
printf(“buffer:%s ”,buffer);
printf(“buffer changed to:%d ”,strlen(buffer));
}

void main()
{
char *str=”We are the hackers of the world”;
printf(“str:%s len:%d ”,str,strlen(str));
function(str);
}

다음과 같이 실행이 문제없이 끝났다.

bash-2.03# ./sbof_ex2
str:We are the hackers of the world
len:31
buffer original len:10
return address is 0x93850408
buffer:We are the h ufff the world
buffer changed to:31

스택 기반 버퍼 오버플로우 찾기
이 처럼 스택 기반 버퍼 오버플로우는 strcat(), strcpy(), sprintf(), vsprintf(), sscanf(), gets() 등의 함수를 사용할 때 발생하기 쉽다. 그 외에도 루프를 돌면서 getc(), fgetc(), getchar() 등을 사용해 문자열을 읽어 올 때 버퍼 크기를 제대로 체크하지 않는 경우에도 많이 발생한다. 또한 루프를 돌면서 문자열을 복사할 때 길이를 잘못 체크했을 경우, 또는 길이와 관련된 변수의 형(type) 변환 실패에 의한 경우 등이 있다. 버퍼 오버플로우를 일으킬 소지가 많은 앞에 열거한 함수들은 주의를 기울여 사용해야 한다.
스택 기반 버퍼 오버플로우를 통해 루트 권한을 얻거나 리모트 시스템에 침투하려면 suid, sgid 프로그램이나 네트워크 데몬에서 스택 기반 버퍼 오버플로우를 찾아야 한다. 수많은 프로그램이 스택 기반 버퍼 오버플로우를 가지고 있지만 suid, sgid 프로그램과 네트워크 데몬, 또는 네트워크 클라이언트 이외의 일반적인 프로그램에서는 보안상 문제가 되지 않는 경우가 많지만 이 분야 역시 해야 할 얘기가 많다. 이 부분에 대해서는 나중에 따로 한 회를 구성해 다룰 계획이다.


춮처: http://www.imaso.co.kr/?doc=bbs/gnuboard.php&bo_table=article&wr_id=44&page=8