포멧 스트링 버그, Format String Bug(1)

포멧 스트링 버그, Format String Bug

포멧 스트링 버그는 printf 함수 등에서 사용되는 포멧 스트링(%d, %s...)을 잘못된 형태로 사용할 경우 발생하는 버그이다. 포멧 스트링은 데이터를 어떤 형식에 따라 입력받거나 출력하기 위하여 사용하는데 형식을 잘못사용하면 메모리의 데이터가 유출되고 변조될 수 있다.
포멧 스트링 버그는 메모리 릭, 메모리 변조 모두 가능하기 때문에 취약점이 발생하면 exploitable할 가능성이 높다.

printf함수는 아래의 두가지 방법으로 사용할 수 있다.

printf(BUFFER)
printf("FORMAT_STRING", BUFFER)

포멧 스트링 버그는 포멧스트링을 사용하지 않고 바로 BUFFER를 출력하는 첫번째 printf 함수 사용법에서 발생한다. 만약 BUFFER 안에 %x, %d등 포멧 스트링이 들어가면 printf함수는 그것들을 포멧스트링으로 보고 두번째 printf 함수 사용법과 같이 동작하여 스택 메모리를 유출시킨다.

예제 코드를 작성하여 첫번째 방법과 두번째 방법의 printf함수를 비교하며 동작시 스택에 쌓이는 데이터들을 확인해보자.
간단한 에코 프로그램이다. 7번째 줄의 printf에서는 포멧스트링 버그가 발생하고 9번째 줄의 printf에서는 포멧스트링 버그가 발생하지 않는다. 

아래는 포멧스트링버그가 발생하지 않는 9번째 줄의 printf 함수 호출 직전의 스택 모양과 ESP가 가리키는 곳이다.



printf 함수를 콜하게 되면 먼저 FORMAT_STRING을 POP한다. ESP는 한칸 내려가 &BUFFER를 가리킨다. 
FORMAT_STRING을 읽으면서 문자열은 그냥 출력하고 포멧스트링을 만난다면 POP을 하면서 BUFFER의 데이터를 4bytes씩 짝에 맞는 포멧스트링 형식으로 출력을 해준다.

EX)
FORMAT_STRING = "HI, %d, %d!!"
BUFFER[] = {10, 20}
BUFFER 메모리에는 0x00000014 0x0000000A.
이 데이터들을 pop하여 FORMAT_STRING과 매핑.

출력 : HI, 20, 10!!



이런식으로 문자열을 하나잡고 출력하면서 포멧스트링(%d, %c..)을 만나면 BUFFER에서 4byte씩 뽑아와서 형식에 맞게 출력을 한다.

이제 포멧스트링버그가 발생하는 9번째 줄의 printf 함수 호출 직전의 스택모양을 확인해보자.



FORMAT_STRING 문자열이 들어가 있던곳에도 BUFFER의 주소가 들어가 있다. 만약 BUFFER안에 %d같은 포멧스트링이 없다면 그냥 BUFFER를 출력하고 버그없이 printf함수가 종료될 것이다. 하지만 포멧스트링이 있다면 위의 제대로 사용된 printf함수와 같이 동작하게 된다. 
왼쪽 사진에서 ESP가 가리키는 BUFFER를 출력해 나가다가 포멧 스트링을 만나게되면 두번째 BUFFER에서 데이터를 뽑아서 형식에 맞게 출력을 해준다. 즉 하나의 문자열(문자+포멧스트링)로 출력을 하면서 데이터를 뽑아 오기까지 한다.




위의 예제 코드를 컴파일 하고 테스트를 통해 확인하여 보자.



바이너리를 실행하고 입력을 AAAA %x %x %x %x로 주었다.

위 화면의 AAAA %x %x %x %x 은 키보드입력값이고 그 다음줄에 나오는
AAAA ffffd6a8 40 41414141 20782520 은 잘못된 포멧스트링 사용으로 인해 메모리 릭이 일어난 것이고 I am safa!, AAAA %x %x %x %x는 정상적인 포멧스트링 사용으로 제대로된 문자열이 출력되었다. 

더 보자. 처음 AAAA는 문자열이니 그냥 출력이 되었고 %x를 만났다. 첫번째 %x는 위의 스택그림의 &BUFFER와 매핑되어 BUFFER의 주소가 출력되었다. ffffd6a8은 BUFFER의 주소이다. 그리고 두번째 %x는 더미같은 40과 매핑되어 40이 출력되었다 그리고 두, 세번째 %x는 BUFFER와 매핑되어 BUFFER 맨 앞에 8bytes가 출력되었다. A는 아스키코드로 0x41이고 스페이스는 0x20, %는 0x25, x는 0x78이다.

여기서 이 버그를 제대로된 취약점으로 승화 시킬수 있다.

잘보면 BUFFER의 앞의 AAAA를 뒤의 %x를 이용해서 출력을 하는 것을 볼 수 있다. 우리의 입력값을 우리가 출력하게 만들수 있게 되었다. 만약에 AAAA에 우리가 원하는 데이터의 주소를 넣는다면 AAAA주소에 있는 데이터를 릭하게 될것이다.


코드를 위와 같이 수정하고 컴파일하여 실행하여 buf1에 들어있는 DEAD문자열을 릭해보자.



BUFFER의 맨 앞부분에 DEAD문자열의 주소값을 엔디언이 맞게 넣어둔다. 그리고 %x를 이용해 BUFFER의 주소를 뽑아서 출력하고 0x40을 뽑아서 출력한다. 그리고 마지막에 %s를 이용해 조금전에 입력한 DEAD의 주소값을 뽑아와서 %s형식으로 출력해준다. DEAD 문자열이 릭되었다.

[    DEAD의 주소     ][    %x    ][  %x ][ %s  ] // 쉘코드 구성
[????(DEAD의 주소)][&BUFFER][0x40][DEAD] // 출력결과

스택을 pop하면서 포멧스트링에 맞게 출력을 하다 우리가 입력한 BUFFER까지 pop을 하여 출력을 하게된다. 그래서 BUFFER에 임의의 주소값을 넣고 %s로 출력을 하면 원하는 메모리를 릭할 수 있다.

이제 %x, %s 이외의 포멧스트링버그의 핵심인 %n에 대해 알아보자.
%n은 현재까지 출력한 문자의 개수를 메모리에 쓰는 포멧 스트링이다. 사용예제를 확인해보자.
이 코드를 실행하면 아래의 화면이 출력된다.



%n으로 &i에 출력한 개수를 쓴다. 위에서 확인했듯이 printf의 두번째 인자인 포멧스트링의 형식의 데이터가 들어있는 스택을 조작할 수 있다. AAAA에 주소를 넣고 %s로 출력시켜 원하는 메모리 주소를 릭 시켰던것 처럼 원하는 메모리주소에 %n을 이용하여 원하는 값을 넣을 수 있다.
%n 은 출력한 값들 개수를 메모리에 쓰는것인데 버퍼의 크기등의 문제로 많은 데이터를 입력하기 어렵다. 원하는 값을 맞추기 위해서는 %10d, %100c 등의 포멧스트링을 사용해야 한다.

%n을 이용해서 원하는 주소의 메모리에 데이터를 쓰는 건 다음 포스팅에서 알아보도록 하자.
힘들다.

댓글 3개: