Pwnable.kr의 uaf 문제를 풀어보려한다..
생각보다 헤맸었다...🙏
우선 SSH로 접속부터 해보자
접속해보면 위와 같이 3개의 파일이 보인다.
우선 파일의 적용된 보호기법부터 확인해보자!
위와 같이 확인할 수 있다!
Canary와 PIE는 미적용되어있고, 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문을 살펴보자..
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 사이즈로 할당된다.
다음 명령어를 실행파면 rax에 있는 값을 꺼내서, rax에 넣어준다. 처음에는 0x614ea0였지만, 이번에는 give_shell의 주소를 가진 0x401570의 주소를 가져온 것을 확인할 수 있다.
그 후 add rax, 8을 통해 introduce() 함수에 접근하는 것을 확인할 수 있다..!
즉, 원래의 m 객체의 Chunk에 give_shell의 주소 가진 0x401570 - 0x8을 넣어주면, give_shell을 호출할 수 있을 것이다!
그럼 이번에는 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을 해주고, 리틀 엔디안 방식으로 값을 가진 파일을 생성하여 인자로 넘겨주면 된다!
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 획득 !!!
※ 참고 URL
-> 16진수 문자열을 바이너리 파일로 생성 : http://bahndal.egloos.com/582830