[Pwnable.kr Prob] uaf

코코·2023년 2월 2일
0

Pwnable.kr

목록 보기
6/10

Pwnable.kr uaf 문제 풀이

Pwnable.kr의 uaf 문제를 풀어보려한다..
생각보다 헤맸었다...🙏


우선 SSH로 접속부터 해보자
접속해보면 위와 같이 3개의 파일이 보인다.
우선 파일의 적용된 보호기법부터 확인해보자!

위와 같이 확인할 수 있다!
CanaryPIE는 미적용되어있고, RELRO도 Partial RELRO만 적용되어있는 것을 확인할 수 있다.

이제 바이너리를 실행시켜보자!
바이너리를 실행시키면, 위와 같이 Segmentation fault가 등장한다..
소스코드를 살펴보자. 소스코드는 아래와 같다..

#include <fcntl.h>
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
using namespace std;

class Human{
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;
}

: 확장자가 c가 아닌 cpp이다!

우선 맨 위부터 살펴보자. 맨 위를 보면 Human Class를 선언해두었고, Man과 Woman은 Human 클래스를 상속받는다.
Human Class에는 private 접근지정자로 give_shell이라는 함수가 존재하는데, 이를 호출해야할 것 같다...
그리고 protected로 age와 name 변수가 있고, public으로 introduce라는 함수를 가지고 있다.

우선 give_shell은 private로 지정되어, Human Class에서만 접근할 수 있다..
또한 age와 name 변수는 protected로 선언되어 상속받은 객체에서만 접근할 수 있다..

이제 main 부분의 코드를 확인해보자. 우선 맨 윗부분에서 Man과 Woman 객체를 각각 m과 w로 생성한다.
이제 while문을 살펴보자..

  • Case 1의 경우, m과 w 객체의 introduce 함수를 호출한다.
  • Case 2의 경우, 바이너리 파일 실행 시 준 첫 번째 인자를 숫자로 가져와서 len에 넣고, len만큼 버퍼를 생성한다. 그 후 두 번째 인자로 넣은 파일의 내용을 버퍼에 저장한다.
  • Case 3의 경우, m과 w 객체를 해제한다.

Allocator(ptmalloc2)의 경우, 메모리를 할당할 때, Free Chunks 중에서 사용가능한 Chunk가 있는지 확인하여 할당한다. 이 때, 요청한 크기와 같은 Chunk가 있다면 해당 Chunk를 할당해준다.


위의 특성을 이용하여, give_shell 함수를 호출하여야 한다...
우선 m과 w 객체를 할당받은 뒤, 3번 메뉴를 통해 해제해준다.
다음으로 m과 w 객체와 같은 크기로 메모리 할당 요청을 하고, introduce 함수가 저장된 위치에 give_shell 함수의 주소를 넣는다.
그 후 1번을 호출하면 give_shell 함수를 호출할 수 있을 것이다!!

그러려면 파일의 내용에 give_shell 함수의 주소를 넣어줘야 할 것이다...!
위를 염두해두고 이제 gdb을 통해 분석해보자 ~ 💪 분석하기 전에 간단하게 test파일(test파일의 내용으로는 문자열 test로 저장)을 생성하고 실행인자로 전달해주자!

r 8 test
: argv[1] = 8, argv[2] = ./test 넣어서 실행!

main에서 m과 w 객체 생성 이후 heap 명령어를 통해 할당된 heap을 살펴보면 위와 같다..

0x21 사이즈를 가진 heap Chunk를 자세히 살펴보면 아래와 같음..
0x19가 저장되어있는 것은 m 객체이며, Chunk를 자세히 보면, give_shell의 주소가 저장되어있다. 즉, m과 w 객체는 0x21 사이즈로 할당된다.

  • Case 1
    1번 메뉴를 통해 접근 시, 레지스터와 코드를 살펴보자. 호출하기 전 RAX 레지스터를 보면, give_shell의 주소를 가지고 있다.

다음 명령어를 실행파면 rax에 있는 값을 꺼내서, rax에 넣어준다. 처음에는 0x614ea0였지만, 이번에는 give_shell의 주소를 가진 0x401570의 주소를 가져온 것을 확인할 수 있다.

그 후 add rax, 8을 통해 introduce() 함수에 접근하는 것을 확인할 수 있다..!

즉, 원래의 m 객체의 Chunk에 give_shell의 주소 가진 0x401570 - 0x8을 넣어주면, give_shell을 호출할 수 있을 것이다!


  • Case 2

그럼 이번에는 Case 3을 거쳐서 Case 2번으로 이동하여 test 파일의 내용이 어떻게 저장되는지 확인해보자.

Case 3을 통해 m과 w 객체를 해제한 뒤, Case 2를 통해 test파일의 내용을 buffer에 저장한 결과이다.
m 객체의 크기와 같은 메모리 크기를 필요하므로, 해제된 Chunk(m 또는 w객체) 중 하나를 할당해준 모습이다..

※ Allocator는 Memory를 16bytes 단위로 할당해줌. 즉, 8byte를 할당요청하면 16bytes + 16bytes(Chunk header) = 32(0x20)이 필요.

test 파일의 내용이 아스키코드로, 리틀 엔디안 방식으로 나타난다!
그러면 우리는 give_shell의 주소를 가진 주소에서 - 0x8을 해주고, 리틀 엔디안 방식으로 값을 가진 파일을 생성하여 인자로 넘겨주면 된다!

  • 최종 Exploit 시나리오는 아래와 같다.
	1. 0x401568을 리틀 엔디안 방식으로 가진 파일 생성
    2. 바이너리 파일 실행(인자로 8과 위에서 생성한 파일을 전달)
 	3. 3번 메뉴 접근, m 객체와 w 객체를 해제
 	4. 2번 메뉴 2번 접근, m 객체와 w 객체가 해제된 2개의 Chunk를 모두 할당하기 위함
 	5. 1번 메뉴 접근 시, give_shell 함수를 호출하여 Shell 획득!

파일의 내용에 Hex 값을 저장하는 방법은 아래와 같다..

echo -e "hex값" > [생성할 파일명]

우선 우리가 넣을 주소는 0x401570 - 0x8 = 0x401568이다! 그러면 해당 주소를 리틀 엔디안 방식으로, 0x68 0x15 0x40 순으로 넣어줘야 한다.

echo -e "\x68\x15\x40\x00\x00\x00\x00\x00" > file

로컬에서 실행 시 성공적으로 Shell을 획득 !!!

🚩 Flag 획득 !!!


이번 문제는 heap과 Chunk의 개념을 확실히 알고 있어야 풀 수 있는 문제였다... 아직 heap과 Chunk는 나와는 거리가 있다.........😂

※ 참고 URL
-> 16진수 문자열을 바이너리 파일로 생성 : http://bahndal.egloos.com/582830

profile
화이팅!

0개의 댓글