gunnew의 잡설

pwnable.kr 17. fsb(Format String Bug) 본문

System Security

pwnable.kr 17. fsb(Format String Bug)

higunnew 2020. 3. 7. 18:47
반응형

 이해하는데 머리가 터질 뻔했다. 이 블로그를 참조하면서 gdb로 며칠 째 직접 예시를 돌려보고 있었는데 블로그 글대로 나오지 않았다. 현재 gcc는 이 버그를 방지하는 장치가 돼있는 모양이다.. (이건 여러 보호 기법이 걸려있기 때문인데, 이를 해제하고 나의 리눅스 환경에서도 fsb를 실험해보기 위한 설정들을 밑에서 설명할 것)

 

참조 : eunice fsb 설명

 

[Format String Attack] 포맷 스트링 공격이란?

Format String Attack [1] Format String Attack 서론 2000년도 후반에 해커들 사이에 큰 반향을 일으키 보고서 하나가 발표되었다. Format String Attack... Format String Attack이란 무엇인가? 이것은 기존에..

eunice513.tistory.com

Format String Bug는 뭘까? 간단히 이해하고 넘어가 보자.

 

원래 printf 함수는 인자를 format string( "Hi, my name is %s\n", name)과 같이 문자열에 특정한 형태로 지정해주는 문자열을 말한다. 만약 name에 Kim이 들어가 있다면 Hi, my name is Kim이 출력될 것이다.

 

 그런데 printf를 이용해서 어떤 특정 문자열 하나만 출력하고 싶다고 하자.

 

가령

char name[] = "Kim keon woo";

을 출력하고 싶다.

 

그러면 printf("%s", name); 이런 식으로 좀 귀찮게 일일이 형식 지정자를 모두 쳐주어야 하는가? 이 경우 좀 더 직관적으로 printf(name); 으로도 가능하다. 그런데 워닝이 뜬다. 다음과 같이 말이다.

 

보안상 문제가 있나 보다.

 

그것은 바로 name을 출력하고자 하는 문자열이 아니라 printf의 함수 인자인, 형식 지정자를 포함한 Format String으로 인식한다는 것이다.


* 형식 지정자 %n *

 

 %n이라는 형식 지정자는 printf에서 해당 지정자가 나오기 전까지 출력된 문자의 개수를 해당 지정자에 매칭 되는 변수에 저장하는 역할을 한다.

 

가령

char name[] = "kim";

int num;

printf("hello, my name is %s %n", name, &num);

을 하면 name이 세 글자이고, {hello, my name is kim }의 글자 수인 22가 num에 저장된다.


* GNU Extension "n$" *

 

 GNU에서 제공하는 extension이 있다. (MSVC에서는 작동하지 않으니 사용하지 말자하.) 바로 n$이다. 여기서 n은 printf로 출력할 인자의 n번째에 곧바로 접근할 수 있다. 모르겠으니 예를 들어보자.

 

다음과 같은 코드가 있다. 

 

#include <stdio.h>

int main() {
	printf("%4$d\n", 10, 20, 30, 40, 50, 60);
    return 0;
}

gcc로 위 파일을 컴파일하고 실행하면 40이 출력된다. 그러니까 형식 지정자 %c, %d같은 것들 사이에 n$만 쏙 집어넣으면 된다.

 

그러나 MSVC에서는 요따구로 출력되니 쓰지 말자.

 

이제 자세한 내용은 소스코드와 디스어셈블리 코드를 보면서 얘기해보자.

 


메모리 보호 기법 해제

 색깔이 좀 촌스럽긴 한데 내가 이것 때문에 사흘을 개고생 했기 때문에 강조하고 싶다. pwnable.kr에서 fsb에 접속해서 보이는 binary file은 다음과 같은 메모리 보호 기법들을 해제하고 컴파일한 결과이다.

 

메모리 보호 기법에 대해서 알고 싶다면
참조 : https://bpsecblog.wordpress.com/2016/05/16/memory_protect_linux_1/

 

1. ASLR(Address Space Layout Randomization) 해제.

 ASLR은 공격자의 메모리 공격을 어렵게 만들기 위해 스택, 힙, 라이브러리 등의 주소를 프로세스 Address Space에 랜덤으로 배치하는 기법을 말하며, 이것이 활성화돼있으면 실행할 때마다 데이터 주소가 바뀐다. 이렇게 해제하도록 하자.

 

 활성화하고 싶다면 뒤에 숫자만 1 또는 2로 바꿔주자. 1은 스택과 라이브러리의 랜덤화 옵션이고, 2는 스택, 라이브러리, 힙의 랜덤화 옵션이다. 

 

2. 컴파일 옵션 설정

  • -m32 : fsb가 32bit 실행파일이니까 우리도 32bit로 만들자. 다만 여러분의 리눅스에 32 bit 패키지들이 안 깔려 있을 것 같으므로 컴파일 시 다음과 같은 오류가 뜬다면 gcc-multilib을 설치하자.sudo apt-get install gcc-multilib
  • -fno-stack-protector : 메모리 보호 기법 중 하나인 SSP(Stack Smashing Protector)를 해제한다. 우리가 이전에 Stack Canary 기법이라고 해서 배웠던 것도 이 기법 중 하나이다.

  • -z execstack : 스택에 실행 권한 설정하는 옵션이다. 기본적으로 [stack]과 [heap] 메모리 영역은 실행 권한이 없기 때문이다. 

  • -no-pie : 메모리 보호 기법인 PIE(Position Independent Executable)을 해제하는 옵션이다. 

 


 

printf("%s", buf) vs printf(buf)

다음과 같은 코드가 있다고 하자.

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
 
int main(int argc, char *argv[]) { 
    char buf[100]; 
    int x; 
     
    for(x=0; x<100; x++) 
        buf[x]=1; 
    if(argc!=2) exit(1); 
    x=1; 
    strcpy(buf, argv[1]); 
    printf("%s", buf); 
}

main함수 안에서 char buf[100]와 int x가 선언되었으므로 main함수 스택 안에 메모리가 잡힐 것이다. 거기에 더해서 printf("%s", buf)가 아닌 printf(buf)로 작성한다면 buf에 형식 지정자를 포함한 format string을 넣어 공격할 수 있다. 앞서 보안상의 문제점은 여기서 등장한다.

 

 printf는 원칙적으로 형식 지정자를 포함한 문자열인 format string에 더해서, 출력할 변수들을 인자로 넘긴다.

만약 printf("%s", buf)를 하고 buf = "AAAA %08x %08x %08x %08x %08x ... "를 담고 있다면 buf에 형식 지정자가 있지만 첫 번째 인자인 "%s"라는 format에 맞추어 buf를 '그대로 출력'한다. 그러니까 결과는 

"AAAA %08x %08x %08x %08x %08x ..."가 될 것이다.

 

다만 특이한 점은 printf를 썼지만 이 경우엔 puts를 호출한다는 점이다. 왜인지는 모르겠다.

 

여기에 breakpoint를 걸고 살펴보면 다음과 같다.

짜잔. 똑같이 나온다.

 


printf(buf)

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
 
int main(int argc, char *argv[]) { 
    char buf[100]; 
    int x; 
     
    for(x=0; x<100; x++) 
        buf[x]=1; 
    if(argc!=2) exit(1); 
    x=1; 
    strcpy(buf, argv[1]); 
    printf(buf); 
}

 반면 이번엔 첫 번째 인자 없이 바로 buf만 넘겼다고 해보자. 이 경우에도 buf에 형식 지정자가 들어가지만 않는다면 정상적으로 출력된다는 것은 글의 앞부분에서 이미 얘기했다. 그러나 buf에 형식 지정자가 들어간다면 상황은 달라진다. printf(buf)에 앞서 buf라는 포인터를 스택에 담는다. 그러나 buf라는 포인터는 현재 스택 포인터인 esp와 그리 멀지 않다. 그 거리가 매우 가까울 것이다. 그럼 디스어셈블리 코드를 살펴보자.

main+114에 breakpoint를 걸고 살펴보자. esp를 살펴보자.

현재 esp에는 0xffffcfb8이 담겨있는데 이는 바로 0xb8만큼 떨어진 곳의 주소이다. 그러니까 buf의 주소인 것이다. 여기까지는 위와 동일하나 여기서부터 printf가 작동하는 방식이 조금 다르다. printf는 이제 Format String을 파싱 하고 일반 문자들을 그대로 출력한다. 하지만 형식 지정자를 만나면? stack에서 pop을 한다! 이것이 fsb의 바로 핵심이다. 이때 pop 되는 것은 stack에서 format string을 가리키는 포인터 바로 다음 부분이다. 따라서 이 예시에서 AAAA다음에 %08x가 7번 나오는데 이에 따라 실제 출력되는 값들은 실제로 스택에 있는 내용들이 된다.

 


vi fsb.c

이제 fsb가 어떤 원리로 작동하는 것인지 알게 되었다. 그럼 바로 코드를 살펴보자.

 

#include <stdio.h>
#include <alloca.h>
#include <fcntl.h>

unsigned long long key;
char buf[100];
char buf2[100];

int fsb(char** argv, char** envp){
        char* args[]={"/bin/sh", 0};
        int i;

        char*** pargv = &argv;
        char*** penvp = &envp;
        char** arg;
        char* c;
        for(arg=argv;*arg;arg++) for(c=*arg; *c;c++) *c='\0';
        for(arg=envp;*arg;arg++) for(c=*arg; *c;c++) *c='\0';
        *pargv=0;
        *penvp=0;

        for(i=0; i<4; i++){
                printf("Give me some format strings(%d)\n", i+1);
                read(0, buf, 100);
                printf(buf);
        }

        printf("Wait a sec...\n");
        sleep(3);

        printf("key : \n");
        read(0, buf2, 100);
        unsigned long long pw = strtoull(buf2, 0, 10);
        if(pw == key){
                printf("Congratz!\n");
                execve(args[0], args, 0);
                return 0;
        }

        printf("Incorrect key \n");
        return 0;
}

int main(int argc, char* argv[], char** envp){

        int fd = open("/dev/urandom", O_RDONLY);
        if( fd==-1 || read(fd, &key, 8) != 8 ){
                printf("Error, tell admin\n");
                return 0;
        }
        close(fd);

        alloca(0x12345 & key);

        fsb(argv, envp); // exploit this format string bug!
        return 0;
}

 

 우선 fd를 이용해서 key에 8 bytes를 random으로 넣는다. 근데 alloca가 뭘까? 찾아보자.

 

 alloca의 caller 스택에, 인자로 넘겨온 size만큼을 할당하는 함수이다. caller가 리턴하면 자동으로 해제되는 모양이다. 그리고 https://www.gnu.org/software/gnulib/manual/html_node/alloca.html에 따르면 alloca는 inline으로 처리된다. 그리고 아무래도 스택에 공간을 할당하는 것이다 보니까 stack overflow도 일으킬 수 있는 모양이다. 하지만 중요한 건 이게 아니다.


fsb에서 공격 포인트는 당연히 printf(buf)가 있는 for loop일 것이다. 그리고 랜덤으로 입력받은 key와 buf2에 있는 값이 일치해야만 shell을 얻을 수 있다.

unsigned long long key;
char buf[100];
char buf2[100];

int fsb(char** argv, char** envp){
        char* args[]={"/bin/sh", 0};
        int i;

        char*** pargv = &argv;
        char*** penvp = &envp;
        char** arg;
        char* c;
        for(arg=argv;*arg;arg++) for(c=*arg; *c;c++) *c='\0';
        for(arg=envp;*arg;arg++) for(c=*arg; *c;c++) *c='\0';
        *pargv=0;
        *penvp=0;

        for(i=0; i<4; i++){
                printf("Give me some format strings(%d)\n", i+1);
                read(0, buf, 100);
                printf(buf);
        }

        printf("Wait a sec...\n");
        sleep(3);

        printf("key : \n");
        read(0, buf2, 100);
        unsigned long long pw = strtoull(buf2, 0, 10);
        if(pw == key){
                printf("Congratz!\n");
                execve(args[0], args, 0);
                return 0;
        }

        printf("Incorrect key \n");
        return 0;
}

 

 구조상 +220이 문제의 for loop의 printf일 것이다. 저기에 중단점을 찍고 스택을 살펴보자.

0x0804a100이 두 번이나 들어갔다. 가만보니 이 주소가 바로 전역변수로 선언된, data segment에서의 buf 주소일 것이다. 그리고 스택을 잘 보면 char*** pargv, ***penvp가 있다. 그곳이 바로 $esp+0x38부분에 있는 0xff9c75d0, 0xff9c75d4이다. 이 주소들은 바로 뒷 부분에 존재하는데 각각 0으로 초기화 되어 있다. 이 역참조 관계를 이용하여 got를 덮어씌우면 된다. 

 

 

그런데 우리는 결국 0x804869f를 실행시켜야 한다. 이를 위해 지난 번에 살펴본 plt와 got를 살펴보자. 우선 디스어셈블리 코드에서도 나오지만 printf의 주소는 0x80483f0이다. 그러면 0x80483f0에는 뭐가 있는지 보자. 

 0x804a004위치에 있는 값을 주소로하여 점프하라는 instruction이 있다. 자, 우리는 그럼 0x804a004 주소에 있는 값을 0x804869f로 덮어씌우면 될 것이다.

 

이제 글의 앞 부분에서 말한 걸 써먹어야 할 때이다. 다음 그림에서 0xff9c75d0에, 그러니까 56 bytes 떨어진 곳이 가리키는 값을 0x804a004로 바꿀 것이다. 그러고 나면 esp로부터 80bytes 떨어진 0xff9c75d0에 0x8048004가 적혀있을 것이다. 이제 이 값을 주소로 활용하여 0x804869f로 바꾸면 될 것이다.


최종 Exploitation

 앞서 %n은 format string으로 지금까지 출력된 문자의 개수를 현재 stack에서 pop이 진행중인 곳에 저장하는 역할을 하는 형식 지정자라고 했다. 또한 n$는 printf의 인자로 넘겨진 format string의 포인터가 있는 부분 (여기서는 ESP)로부터 n만큼 떨어진 인자를 가리킨다고 했다. 따라서 esp로부터 14번째 떨어진 곳이 가리키는 값을 바꾸기 위해서는 %14$n을 붙여주어야 한다. 이때 이 위치에 있는 값이 가리키는 곳을0x804a004(134520836‬)로 바꾸어야 하기 때문에 

 

%134520836c%14$n

을 입력한다.

주의할 점은 134520836 다음에 c를 입력하여 1 byte character를 134520836개 출력하게끔 한다는 점이다.

 

두 번째는 이것과 동일하다.

사진을 다시 참조하자.

첫 번째 동그라미 0xff9c75d0은 esp로부터 20번째 떨어진 곳을 가리키고 있으므로

덮어씌우고자 하는 값  0x804869f(134514335‬)를 씌운다.

%134514335‬c%20$n

 

 



 아 너무 어려웠다. 이 문제를 보고 나서 지금까지 장장 나흘이 흘렀고 긴 시간에 걸쳐 이 write-up을 완성하였다. 그러나 이 write-up도 설명이 너무 부족한 것 같다. 스터디원 중 한 분은 직접 key와 buf2에 접근하여 푸신 것 같은데 그걸로도 풀어보려 했지만 더 이상은 힘들어서 못하겠다. 나중에 힘이 닿는다면 도전해 봐야지...

반응형

'System Security' 카테고리의 다른 글

pwnable.kr 18. ascii_easy (ROP)  (0) 2020.03.15
pwnable.kr 16. uaf (Using After Free)  (0) 2020.02.19
pwnable.kr 15. cmd2  (0) 2020.02.19
pwnable.kr 14. cmd1 (Wildcard)  (0) 2020.02.17
pwnable.kr 13. lotto  (0) 2020.02.17
Comments