gunnew의 잡설

pwnable.kr 16. uaf (Using After Free) 본문

System Security

pwnable.kr 16. uaf (Using After Free)

higunnew 2020. 2. 19. 22:07
반응형

 이번 문제는 정말 어려웠다. 그래도 문제를 풀면서 나 스스로에게 조금은 뿌듯했던 점이 있다. 어느 정도 접근 법도 맞았고, 템빨이긴 하지만(peda를 써서 읭(?) 하는 부분이 있었음. 추후 설명) exploitation point도 캐치하였다는 점이다. 물론 아직 gdb를 능숙히 다룰 수 있는 실력이나 어셈블리 코드를 분석하는 능력은 부족하지만, 까막눈이었던 몇 주 전과 비교하면 꽤나 나아진 느낌이다.

 

 일기는 여기서 그만 쓰고 처음부터 차근차근해보자.


pdisas main

 


디스 어셈블리 코드를 보기 전, 한 가지 팁이 있다. 다음과 같이 디스 어셈블리 코드가 깨지는 경우

 

set print asm-demangle on

를 치면 깨끗해진다.

 

                         ↓↓


gdb uaf

 

 * uaf.cpp *

#include <fcntl.h>
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>

using namespace std;

private:
        virtual void give_shell(){
                system("/bin/sh");
        }
protected:
        int age;
        string name;
public:
        virtual void introduce(){
                cout << "My name is " << name << endl;
                cout << "I am " << age << " years old" << endl;
        }
};

class Man: public Human{
public:
        Man(string name, int age){
                this->name = name;
                this->age = age;
        }
        virtual void introduce(){
                Human::introduce();
                cout << "I am a nice guy!" << endl;
        }
};

class Woman: public Human{
public:
        Woman(string name, int age){
                this->name = name;
                this->age = age;
        }
        virtual void introduce(){
                Human::introduce();
                cout << "I am a cute girl!" << endl;
        }
};


int main(int argc, char* argv[]){
        Human* m = new Man("Jack", 25);
        Human* w = new Woman("Jill", 21);

        size_t len;
        char* data;
        unsigned int op;
        while(1){
                cout << "1. use\n2. after\n3. free\n";
                cin >> op;

                switch(op){
                        case 1:
                                m->introduce();
                                w->introduce();
                                break;
                        case 2:
                                len = atoi(argv[1]);
                                data = new char[len];
                                read(open(argv[2], O_RDONLY), data, len);
                                cout << "your data is allocated" << endl;
                                break;
                        case 3:
                                delete m;
                                delete w;
                                break;
                        default:
                                break;
                }
        }

        return 0;
}

 자 일단 소스코드를 좀 살펴보자. 우리가 바이너리 파일을 실행한 후

 

1을 입력하면 남자와 여자가 각각 자기소개를 한다.

 

2번을 입력하면 두 번째 인자로 넘겨온 숫자만큼 동적 할당을 하고 세 번째 인자로 넘어온 파일을 열어서 그 길이만큼 데이터를 읽어온다.

 

3을 입력하면 m, w의 동적 할당이 해제된다. 

 

우리의 목표는 Human 클래스에 있는 give_shell 함수에 접근하는 것이다. 그런데 현재 사용할 수 있는 함수는 introduce이다. 이 함수가 이 문제의 열쇠가 될 것임은 자명하다! 그럼 introduce를 호출하는 1번 부분을 잘 살펴보자.

   0x0000000000400fb5 <+241>:	cmp    eax,0x2
   0x0000000000400fb8 <+244>:	je     0x401000 <main+316>
   0x0000000000400fba <+246>:	cmp    eax,0x3
   0x0000000000400fbd <+249>:	je     0x401076 <main+434>
   0x0000000000400fc3 <+255>:	cmp    eax,0x1
   0x0000000000400fc6 <+258>:	je     0x400fcd <main+265>
   0x0000000000400fc8 <+260>:	jmp    0x4010a9 <main+485>
   0x0000000000400fcd <+265>:	mov    rax,QWORD PTR [rbp-0x38]
   0x0000000000400fd1 <+269>:	mov    rax,QWORD PTR [rax]
   0x0000000000400fd4 <+272>:	add    rax,0x8
   0x0000000000400fd8 <+276>:	mov    rdx,QWORD PTR [rax]
   0x0000000000400fdb <+279>:	mov    rax,QWORD PTR [rbp-0x38]
   0x0000000000400fdf <+283>:	mov    rdi,rax
   0x0000000000400fe2 <+286>:	call   rdx
   0x0000000000400fe4 <+288>:	mov    rax,QWORD PTR [rbp-0x30]
   0x0000000000400fe8 <+292>:	mov    rax,QWORD PTR [rax]
   0x0000000000400feb <+295>:	add    rax,0x8
   0x0000000000400fef <+299>:	mov    rdx,QWORD PTR [rax]
   0x0000000000400ff2 <+302>:	mov    rax,QWORD PTR [rbp-0x30]
   0x0000000000400ff6 <+306>:	mov    rdi,rax
   0x0000000000400ff9 <+309>:	call   rdx
   0x0000000000400ffb <+311>:	jmp    0x4010a9 <main+485>

+265 ~ +309까지 구조를 잘 살펴보자. 

                        case 1:
                                m->introduce();
                                w->introduce();
                                break;

이 대칭적인 구조와 뭔가 들어맞는다. rdx에 있는 값을 통해 어떤 함수를 호출한다. 그럼 역으로 rdx에는 무슨 값이 들어갔는지 살펴보자.

 

+286에서 rdx값은 +276 : mov rdx, QWORD PTR [rax]에서 온다.

이것은 rax에 들어있는, 어떤 주소에 있는 값을 가져오는 것이다.

 

그럼 rax에는 무엇이 들어있는가? 일단 +265 mov rax, QWORD PTR [rbp-0x38]로 rbp에서 0x38만큼 떨어진 스택 어딘가에 있는 값을 가진다. 그것을 주소로 하는 부분의 값을 rax에 가져온다. 

 

자 그럼 다시. rbp-0x38 에는 뭐가 들어있을까? 분명 main()의 지역 변수가 들어가 있을 것이다!

 


 3번 (Free)

 3번을 호출할 때 코드로 넘어와 보았다. 잘 보니 rbp-0x38에 있는 값을 rbx로 옮기고 뭐 테스트를 한 다음에 소멸자를 호출한다. 가만히 보아하니 rbp-0x38에는 Man과 Woman이라는 클래스 지역 변수가 들어가 있었나 보다.

 

   0x0000000000401076 <+434>:	mov    rbx,QWORD PTR [rbp-0x38]
   0x000000000040107a <+438>:	test   rbx,rbx
   0x000000000040107d <+441>:	je     0x40108f <main+459>
   0x000000000040107f <+443>:	mov    rdi,rbx
   0x0000000000401082 <+446>:	call   0x40123a <Human::~Human()>
   0x0000000000401087 <+451>:	mov    rdi,rbx
   0x000000000040108a <+454>:	call   0x400c80 <operator delete(void*)@plt>
   0x000000000040108f <+459>:	mov    rbx,QWORD PTR [rbp-0x30]
   0x0000000000401093 <+463>:	test   rbx,rbx
   0x0000000000401096 <+466>:	je     0x4010a8 <main+484>
   0x0000000000401098 <+468>:	mov    rdi,rbx
   0x000000000040109b <+471>:	call   0x40123a <Human::~Human()>
   0x00000000004010a0 <+476>:	mov    rdi,rbx
   0x00000000004010a3 <+479>:	call   0x400c80 <operator delete(void*)@plt>
   0x00000000004010a8 <+484>:	nop
   0x00000000004010a9 <+485>:	jmp    0x400f92 <main+206>
   0x00000000004010ae <+490>:	mov    r12,rax
   0x00000000004010b1 <+493>:	mov    rdi,rbx
   0x00000000004010b4 <+496>:	call   0x400c80 <operator delete(void*)@plt>
   0x00000000004010b9 <+501>:	mov    rbx,r12

1번에서 공격할 부분 알아내기 (call rdx? rdx에는 뭐가 있을까?)

   0x0000000000400fb5 <+241>:	cmp    eax,0x2
   0x0000000000400fb8 <+244>:	je     0x401000 <main+316>
   0x0000000000400fba <+246>:	cmp    eax,0x3
   0x0000000000400fbd <+249>:	je     0x401076 <main+434>
   0x0000000000400fc3 <+255>:	cmp    eax,0x1
   0x0000000000400fc6 <+258>:	je     0x400fcd <main+265>
   0x0000000000400fc8 <+260>:	jmp    0x4010a9 <main+485>
   0x0000000000400fcd <+265>:	mov    rax,QWORD PTR [rbp-0x38]
   0x0000000000400fd1 <+269>:	mov    rax,QWORD PTR [rax]
   0x0000000000400fd4 <+272>:	add    rax,0x8
   0x0000000000400fd8 <+276>:	mov    rdx,QWORD PTR [rax]
   0x0000000000400fdb <+279>:	mov    rax,QWORD PTR [rbp-0x38]
   0x0000000000400fdf <+283>:	mov    rdi,rax
   0x0000000000400fe2 <+286>:	call   rdx
   0x0000000000400fe4 <+288>:	mov    rax,QWORD PTR [rbp-0x30]
   0x0000000000400fe8 <+292>:	mov    rax,QWORD PTR [rax]
   0x0000000000400feb <+295>:	add    rax,0x8
   0x0000000000400fef <+299>:	mov    rdx,QWORD PTR [rax]
   0x0000000000400ff2 <+302>:	mov    rax,QWORD PTR [rbp-0x30]
   0x0000000000400ff6 <+306>:	mov    rdi,rax
   0x0000000000400ff9 <+309>:	call   rdx
   0x0000000000400ffb <+311>:	jmp    0x4010a9 <main+485>

 다시 1번으로 돌아가서 b *main+272를 통해 중단점을 걸고 실행한 다음 rax 값을 살펴보자. rax에는 0x401570이라는 주소 값이 있다. 그럼 main+276에서 mov rdx, QWORD PTR [rax]을 통해 최종적으로 rdx에 어떤 instruction을 전달해준다. 그럼 QWORD PTR [rax]을 통해 전달되는 값은 뭘까?

 

0x4012d2이다.

 

결국 call rdx는 call 0x4012d2이므로 0x4012d2는 instruction이다. 우리가 앞서 예상한 대로라면 introduce일 것이다.

예상 적중! introduce가 있다.

 

오잉? 잘 생각해보면 굳이 add rax, 0x8을 할 필요가 있었을까? 그럼 8을 더하기 전엔 뭐였을까? 살펴보자.

 

찾았다!! give_shell함수이다. 그러니까 결국 우리는 0x40117a라는 주소로 점프하거나 이 주소를 call 하는 방식으로 exploit 해야 한다. 그러면 우리는 여기로 어떻게 점프를 할 수 있을까? 뭔가 2번에서 동적 할당하는 부분이 의심스럽다. 


AAAA를 넘겨준 다음 free 해보기

 

실행을 해보자. 우선 그에 앞서 테스트를 위해 /tmp에 test.txt를 만들고 AAAA라는 값을 입력하고 난 다음 두 번째 인자로 /tmp/test.txt를 넘겨주었다. 요렇게 말이다.

 

 

 자 일단, 1, 2, 3번 각각 case 블록이 끝나는 곳에 중단점을 건다.

 처음에는 1번, 그러니까 use를 하여 자기소개를 한 상태이다. 위에 스택 정보에서도 알 수 있듯이 give_shell을 부르는 주소가 0x401570, 0x401550으로 두 개가 있는 상태이다.

이제 해제를 해보자.

 

스택 정보에서도 확인할 수 있듯이 rbp-0x38과 rbp-0x30의 값 자체는 그대로이다. 그러나 그 값을 주소로 삼았을 때 타고 들어가 보면 값이 0으로 초기화되어 있음을 알 수 있다. 이렇듯 해제를 했을 때 rbp-0x38과 rbp-0x30의 값이 그대로 남아있다는 것을 활용해야 한다.

 


"After Free"

 우리는 파일을 읽어 들여서 data에 "AAAA"라는 문자열을 담을 것이다. 이제 2번을 한번 해보자. 그러면 다음과 같이 나온다.

 

 

 Man을 가리키는 객체 포인터 rbp-0x38은 0x10d8c50이다. 

 

 Woman을 가리키는 객체 포인터 rbp-0x30은 0x10d8ca0이다.

 

 그러고 char *data를 가리키는 포인터는 rbp-0x28인데 오잉?? rbp-0x30과 rbp-0x28의 값이 같아졌다. 여기에는 위에서도 보다시피 "AAAA"가 저장되어 있다.

 

 data가 가리키는 포인터는 분명히 "AAAA"를 담고 있어야 한다. 그런데 우리는 방금 Man과 Woman을 해제했다. 맞지? 3번 눌렀으니까 마지막으로 Woman이 해제되었을 것이다. 그러고 나서 Human 객체는 아니지만 char 배열에 4 bytes를 할당해주려고 했다. 근데 glibc의 특성상 해제된 메모리를 버리는 것이 아니라 bin이라는 일종의 '큐'가 collecter역할을 하고 있다. 간단히 이유를 설명하자면 새로운 힙 영역을 요청하는 것이 큰 오버헤드가 들기 때문이다. 따라서 같은 크기의 객체를 할당하게 되면 다른 영역에 할당되는 것이 아니라, 이전에 해제된 메모리 영역이 있다면 그것을 그냥 재활용한다. 그래서 가장 마지막에 해제된 메모리 영역이 0x10d8ca0(rbp-0x30에 있는 값. Woman 객체의 포인터)이기 때문에 char* data에 0x10d8ca0이 그대로 들어갔다. 

 

 자! 이제 끝났다. 그러면 만약 이 상태에서 그대로 '2 input'인 after를 통해 할당을 요청한다면 어떻게 될까? ㅇ이전에 해제되었던 Woman이 있던 자리는 이미 방금 전에 할당이 되었으므로 Man이 있던 자리가 최근 해제된 메모리 영역이 되어 0x10d8c50d (rbp-0x38에 있는 값. Man 객체의 포인터)에 할당받게 될 것이다.

 

 백문이 불여일견. 확인해보자.

둘 다 "AAAA"가 들어갔음을 알 수 있다.

 

이제 끝났다. 처음에 우리가 introduce를 할 때 어떻게 give_shell을 얻어왔는지 생각해보자. 처음에 rbp-0x38에 있는 값을 가져오지 않는가?? 위에서 나는 이렇게 얘기했다.

그럼 rax에는 무엇이 들어있는가? 일단 +265 mov rax, QWORD PTR [rbp-0x38]로 rbp에서 0x38만큼 떨어진 스택 어딘가에 있는 값을 가진다. 그것을 주소로 하는 부분의 값을 rax에 가져온다. 

그런데 우리는 두 번 after를 해줌으로써 rbp-0x38에 우리가 원하는 값을 입력할 수 있었다.

 

네 줄 요약
1. 두 번째 인자로 우리가 침투하고 싶은 주소(여기서는 give_shell의 주소겠지?)를 담은 파일 경로를 넣고 실행한다.
2. 숫자 3을 입력하여 free를 한 번 해준다.
3. 두 번에 걸쳐 2를 눌러서 두 번 동적할당 해준다.
4. 숫자 1을 입력하여 실행해주면 끝.

 

이제 진짜 마지막이다. 그럼 우리가 침투하고 싶은 주소가 뭐였지?

 

이 친구이다. 그러니까 0x401570이 최종적으로 전달되어야 한다. 근데 add rax, 0x8이 기억나는가? 그러니까 우리가 입력해야 할 주소는 0x401570 - 0x8이다. 0x401568이 전달되어야 한다. 이제 ㄹㅇ 끝임.

 

 

give_shell에서 shell의 접근 권한을 얻었으므로 cat flag를 해주면 된다. 아이고 힘들었다.

반응형

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

pwnable.kr 18. ascii_easy (ROP)  (0) 2020.03.15
pwnable.kr 17. fsb(Format String Bug)  (0) 2020.03.07
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