티스토리 뷰

[포맷스트링 취약점]정리


아래의 소스는 아스키 코드표를 출력하는 예제이다. 메모리에 저장된 값과 모니터를 통해 보는 것이 다르다는 것을 보기 위해서 설명을 해보겠다. value 배열에 정수값을 넣었지만 실제로 메모리에는 2진수 값이 들어 있다. 메모리나 하드디스크는 전기가 있고 없음만을 판단할 수 있어서 0과 1로만 데이터를 저장할 수 있기 때문이다. 즉, 화면으로 "a"라는 문자를 본다고 해서 메모리에 "a"라는 문자가 저장돼 있는 것은 아니고, 화면에서 "1"이라는 숫자가 표시된다고 해서 메모리에 "1"이라는 숫자가 저장돼 있지는 않다는 의미이다.


결과 값을 보면, 0부터 계속하여 126까지 Hex와 DEC와 OCT, CHAR을 출력한다. 0에서 31번까지에 해당하는 특수문자는 %c라는 포맷스트링을 통해 문자로 출력할 수 없기 때문에 깨진 것 처럼 보인다. 그러나 32에서 126번까지는 %c 포맷스트링을 통해 표현할 수 있는 문자이므로 정상 문자가 출력이 되는 것을 볼 수 있다. 그러므로 메모리에 저장된 2진수와 화면상에 표시되는 값을 동일한 값으로 혼동해서는 안 된다.


일반적으로 C 프로그래밍 언어에서 제공하는 대표적인 포맷스트링을 정리하면 아래와 같다. 앞으로는 이러한 포맷스트링과 관련된 코드를 볼 때 단순히 화면에 출력되는 값뿐 아니라 메모리에 실제로 어떤 값이 저장돼 있는지도 알아야 한다.

식별자 

인수 

출력 

%x 

int  

부호 없는 16진 정수 

%d 

int  

부호 있는 십진 정수 

%o  

int  

부호 없는 8진 정수 

%c 

char 

1문자 

%s 

char * 

"\0" 직전까지의 문자열 

%f 

double 

소수점 표현 가능 

%p 

void * 

변수의 주소 16진수 출력 


아래 소스는 포맷스트링 인자를 사용하지 않는 코드이다. 포맷스트링 취약점은 프로세스가 구성하는 메모리 구조를 정화가하게 분석할 수가 있어야 하므로, 메모리 구성이 어떻게 되는지 gdb로 보겠다.


gcc로 컴파일을 할 때 -mpreferred-stack-boundary=2 옵션을 지정할 경우에 스택의 경계(Boundary)가 2바이트 단위로 증가하도록 설정한다.


위의 소스를 디스어셈블해서 보면 main+3 부분에서 지역 변수 공간을 0xc(12 byte)만큼 할당한 것을 볼 수가 있다.


포맷스트링 취약점이 있는 경우 어떤 현상을 보이는지 확인해 보면서 실제 메모리 구조를 그려보겠다.


<--------- 스택 증가 방향

4

*suspect2 

*suspect 

int value

SFP 

RET 

argc 

argv 

env 

 문자열 주소

문자열 주소 

30 

 

 

명령어 

환경변수 

<--------- 낮은 메모리 주소                           높은 메모리 주소 ---------->


위의 소스로 다시 가서 포맷스트링 취약점의 개념을 짚고 넘어가자면, printf(argv[1])부분인데, 원래는 printf("%s", argv[1])가 되어야 소스가 완성이 된다. 하지만 "%s"를 쓰지 않고도 이 소스는 실행이 아래와 같이 잘 된다.


하지만 소스코드에는 분명히 포맷스트링 지정자가 없기 때문에 입력값에 포맷스트링 지정자를 입력하면 입력한 포맷스트링 지정가가 인식되어 스택을 살펴볼 수 있게 된다. 하지만 시스템마다 패턴이 다르므로 충분한 테스트를 거쳐 몇 번째 %x 포맷스트링 지정자에서 우리가 입력한 값이 보이는지 확인해야 한다.

포맷 스트링 지정자를 입력해 보니 각각 어떤 값이 표시된다. printf("%x")가 바로 처리되기 때문에 이처럼 이상한 동작이 일어나는 것이다. %x를 인자로 하고 실행을 해보면 메모리 주소 같은게 계속 나온다. 연속적으로 %x를 주었더니 개수만큼 메모리 주소와 같은 문자열이 출력된다. 3번째와 6번째의 %x 지정자에서 "le(30)"와 "2"라는 다른 출력값과 자릿수가 다른 것이 보인다. 다시 작성하여 자릿수를 주변의 8자리 주소값 패턴과 동일하게 보이도록 입력하겠다.


이제 이렇게 출력된 값을 디버깅하면서 main() 함수의 스택 프레임을 정확하게 확인해 보겠다. 일단 printf(argv[1]) 부분을 출력하는 부분에 BP를 건다. 그리고 r 명령어로 %8x의 8개 인자로 실행을 하면 printf 함수에서 멈추게 된다. 현재 스택을 보고 싶기 때문에 x/9x $esp로 9개의 16진수를 출력한다.


현재 스택에 있는 값들을 차례대로 문자열을 확인해 보면 아래와 같이0x804841a와 0x8048410에 문자열이 2개가 있는 것을 확인할 수가 있다.


int value = 1e(30)값이 보이고, 0xbfffe548(스택 프레임 포인터(SFP))와 0x42015574(RET(리턴 주소))가 연속적으로 위치한 것을 볼 수가 있다. 0xbfffe530 에는 인자수(argc)가 들어있는 것을 볼 수가 있다.


0xbfffe574는 명령어 입력(argv)이다. argv[0]은 실행 파일이고, argv[1]은 인자로 주었던 것을 확인 가능하다.


0xbfffe580(환경변수(env))가 있다. 따라서 들어가보면 정보가 나열되어 있다.


위와 같이 추측했던 스택의 구조 대로 메모리에 데이터가 들어있는지 확인할 수가 있다. 위의 정보들을 정리해 보면 아래와 같다.

<--------- 스택 증가 방향

4

*suspect2(0xbfffe51c) 

*suspect(0xbfffe520) 

int value(0xbfffe524)

SFP 

RET(0xbfffe52c) 

argc 

argv 

env 

 0x804841a

0x8040410 

0x000000le

 

 0x42015574

명령어 

환경변수 

<--------- 낮은 메모리 주소                           높은 메모리 주소 ---------->


지금까지 분석한 내용에서 이상한 점이 있다면 아래에서 출력한 결과를 보면 printf() 함수를 위한 리턴주소(0x08048350) 다음에 "%8x %8x %8x %8x %8x %8x %8x %8x"라는 문자열의 주소가 있어서 입력한 문자열이 스택에 푸시돼 있다는 것이다. 이것은 printf() 함수를 호출하기 전에 printf() 함수에 인자를 전달하기 위해 스택에 인자값을 푸시했기 때문이다.


위의 printf 함수 2개에 BP를 걸고 "%8x 8x 8x 8x 8x 8x 8x 8x"를 인자로 하고 실행하고, stepi 명령어로 printf 함수로 들어가서 esp를 확인한다. 확인을 하면 0xbfffdf24부분에 printf함수의 리턴함수 0x08048350 값이 들어가있고 0xbfffdf28부분에는 0xbffffc14가 들어가있는데, 이 값을 확인을 해보면 인자로 입력한 "%8x 8x 8x 8x 8x 8x 8x 8x" 값인 것을 확인 가능하다.


위와 같이 하면 지금까지 main() 함수에서 만들어진 스택의 상단에 인자값과 리턴주소(RET)가 스택에 푸시되면서 아래와 같은 스택의 모양을 갖추게 된다.

<--------- 스택 증가 방향

4

RET

(0xbfffdf24)

인자 전달

(0xbfffdf28) 

*suspect2(0xbfffe51c) 

*suspect(0xbfffe520) 

int value(0xbfffe524)

SFP 

RET(0xbfffe52c) 

argc 

argv 

env 

0x08048350

"%8x...%8x"

 0x804841a

0x8040410 

0x000000le

 

 0x42015574

명령어 

환경변수 

<--------- 낮은 메모리 주소                           높은 메모리 주소 ---------->


그리고 위의 printf("%8x 8x 8x 8x 8x 8x 8x 8x") 함수를 실행하고 나서 화면에서 <main+40>에 있는 add 명령이 실행되면서 다음의 printf("\n")를 처리하기 위한 메모리 공간도 앞에서 사용한 공간을 그대로 재활용하게 된다. 0x08048358 주소의 printf문도 위의 표와 똑같이 RET는 0x0804835d 가 되고, "\n" 값이 인자 전달 값으로 가서 add 명령으로 인해 그대로 재활용이 된다. 

<--------- 스택 증가 방향

4

RET

(0xbfffeb24)

인자 전달

(0xbfffeb28) 

*suspect2(0xbfffe51c) 

*suspect(0xbfffe520) 

int value(0xbfffe524)

SFP 

RET(0xbfffe52c) 

argc 

argv 

env 

0x0804835d

"\n"

 0x804841a

0x8040410 

0x000000le

 

 0x42015574

명령어 

환경변수 

<--------- 낮은 메모리 주소                           높은 메모리 주소 ---------->


이처럼 스택에서는 푸시와 팝을 하면서 메모리 공간을 효율적으로 쓰고 있다.


밑의 명령과 같이 포맷스트링 취약점이 있는 프로그램은 메모리 구조를 볼 수 있게 된다는 취약점이 있다.


특정 메모리에 원하는 값을 쓰고 싶다면 포맷스트링 공격의 핵심에 해당하는 %n 지정자를 이용하면 된다. 아래와 같이 실제로는 메모리에 원하는 값을 쓸 수 있는 고급 포맷스트링 지정자도 있다. 

 식별자

인수 

설명 

사용법 

%n 

int * 

%n 이전까지 쓴 문자열의 바이트 수 쓰기 

%바이트수c%n 

%hn 

short * 

%hn 이전까지 쓴 문자열의 바이트 수 쓰기 

%바이트수c%hn 


위와 같이 포맷스트링은 메모리의 값을 읽을 수 있을뿐더러 메모리에 원하는 값을 쓸 수도 있다. %n 지정자는 지정자 앞에 쓰인 문자의 수를 %n 지정자에 표기된 바이트 수만큼 이동한 메모리에 있는 주소에 값을 쓴다. 아래의 소스는 %n 지정자를 사용한 소스이다. int 타입의 변수 i와 char 배열인 str이 있고, printf(str)에서 포맷스트링 취약점이 있는 소스코드가 있다.


int 타입의 변수 i와 char 배열인 str이 있고, printf(str)에서 포맷스트링 취약점이 있는 소스코드가 있다. 이 소스코드를 바탕으로 %n 지정자를 이용해 어떤 방식으로 원하는 메모리에 의도한 값을 넣을 수 있는지 확인하기 위해, 컴파일하고, 실행해본 결과, 네 번째 %x 지정자에서 첫 번째로 입력한 "AAAA" 문자열이 있음을 볼 수 있다. 그리고 변수 i의 주소가 0x8049484이며, 여기에 저장돼 있는 값이 0인 것을 확인할 수 있다. 


위의 출력 결과를 확인하면 "AAAA" 문자열 뒤에 있는 %8x 지정자에 대한 반응으로 "AAAA %8x %8x %8x %8x" 포맷스트링 문자열보다 먼저 스택에 푸시된 값을 순차적으로 읽어 오는 것을 볼 수가 있다.

gdb로 확인해 본 결과 0x08048398은 RET이고, 0xbffffc18은 "AAAAA %8x %8x %8x %8x"가 들어가있다.


<------ 스택 증가 방향

 4

0xbffff24

 0xbffff28

 0xbffff2c

 0xbffff30

0xbffffc18 

 0

0 

AAAA 

<------ 낮은 메모리 주소                                          높은 메모리 주소------->


그렇다면 여기서 %n 지정자를 이용해 어떻게 값을 쓸 수 있는지 확인을 해보겠다. 예를 들어서 앞에 인자로 입력한 "AAAA %8x %8x %8x %8x" 문자열을 "AAAA%8x%8x%8x%n"과 같이 입력하면 어떻게 될지 생각을 해봐야 한다.


"AAAA"를 입력한 후 %8x 지정자가 3개 있으므로 위의 스택에서 맨 아래에 있는 0xbffff30 주소로 스택포인트(SP)가 이동해 있으므로 그곳에 저장돼 있는 문자열인 "AAAA"의 16진수 값인 0x41414141에 해당하는 주소에 strlen(AAAA%8x%8x%8x)의 결과에 해당하는 0x1c(28)을 쓰려고 할 것이다. 


그러나 0x41414141이라는 주소에는 접근이 불가능 하므로 에러가 발생할 것이다. 그러므로 다음과 같이 "AAAA"라는 문자열 대신 우리가 쓸 수 있는 메모리 주소를 지정하고, 문자열의 수를 조절하면 원하는 메모리 주소에 원하는 값을 쓸 수 있다.

결과를 보면 0x8049484라는 주소에 있는 변수 i의 값이 바뀐 것을 볼 수가 있다. 즉, 덮어쓸 메모리 주소를 정확하게 알고 있다면 원하는 값을 해당 메모리 주소에 덮어쓸 수 있는 것이다.



응용을 해서 한다면 메모리를 파괴하는 것도 가능할 것이다. 좀 더 공부한 뒤에 시도를 해봐야 겠고, 포맷스트링 취약점은 BOF 공격에 비해 원리나 계산이 상당히 복잡하지만 분명 흥미로운 취약점이다.



'시스템해킹 > 정리' 카테고리의 다른 글

eggshell  (0) 2015.08.01
shellcode  (0) 2015.08.01
Buffer overflow 문서  (0) 2015.07.23
[/etc/passwd] [/etc/shadow] 구조  (0) 2015.07.22
SetUID, SetGID, Sticky Bit  (0) 2015.07.14
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함