pwnable kr-uaf
왠만하면 풀이를 안올리려 했는데 올린사람이 많이 없어 올려보겠습니다. 저도 오늘 푼 문제인데 시간이 많이 걸렸네요....
- #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;
- }
소스 코드입니다.
virtual 로 선언된 함수가 있어 Vtable 개념으로 푸는 문제라 생각해고 꽤 오래 생각했는데 아닌거 같았습니다.(맞았나....)
virtual 같이 가상함수를 사용하는 경우 Vtable이 생성되고, 이 가상함수 테이블에 저장된 함수 포인터를 이용하여 각 메소드에 접근하게 됩니다.
Vtable의 포인터는 객체의 메모리의 첫 4바이트에 위치합니다.
이 포인터를 이용하여 가상함수를 찾고, 가상함수 테이블에서 virtual로 생성된 함수에 해당하는 주소를 찾아서 실제함수를 호출하게 되는 것입니다.
즉 Vtable은 제 나름대로 알기 쉽게 정리하자면, virtual로 정의된 함수들이 모여있는 곳입니다.
여기서는 위에서의 개념대로 나름 풀었던 것 같습니다.
gdb 로 디버깅을 해보았는데 중요한 부분만 보도록 하겠습니다.
위에서 보는 switch(op) 구문입니다. 입력해준 숫자로 분기합니다.
1번을 눌렀을 때의 분기지점입니다. rbp-0x38이 Man, rbp-0x39가 Woman인데 Man을 브레이크 포인트 걸고 흐름을 보았습니다.
위 사진을 보면 0x00401570에 0x0040117a 가 있습니다.
이는 위에서의 give_shell() 함수입니다.
그리고 위위 사진을 보면 rdx=rax+8을 하고 그 주소에 있는 함수를 호출합니다.
이는 rax=401570+8=0ㅌ00x401578안에 있는 0x004012d2를 호출하는 구문입니다.
여기서 눈치를 채야합니다.
저기서 호출하는 함수는 Human 클래스에 있는 intruduce() 임을 알 수가 있습니다.
그럼 8을 왜 더하는 걸까요??
이는 인스턴스를 만들었기 때문입니다.
- protected:
- int age;
- string name;
따라서 주소 0x00401570에 위치한 함수 0x0040117a 는 give_shell() 가 되는 것입니다.
여기서 주목해야 할점이있습니다.
8을 더한 0x00401578을 호출한다??? 그렇다면 0x00401570을 호출하면 그 안에있는 give_shell()가 실행되겠네요??
그럼 0x00401570 보다 8 작은 0x00401568를 0x00401570 대신 써주면 결국 호출되는 것은 0x00401570이 됩니다.
그럼 어디다 써주어야 할까요??
여기서 많은 시간을 소비했습니다.
그래서 switch(op)에 2를 입력하면, 실행되는 파일에서 데이터를 읽어와 어딘가에 저장하는 코드를 이용하기로 했습니다.
한번 시험해 보도록 하겠습니다.
이 개념은 인터넷에 많이 나와있지만 여기서는 free, 즉 인스턴스 할당을 취소하고, 다시 인스턴스를 만들때 참조되는 곳을 공격자가 임의로 조작하는 것입니다.
/tmp/zjet 파일에는 AAAA를 써 넣었씁니다.
보는 것처럼 3, 즉 free를 하고 2를 입력해서 할당해서 레지스터를 보았습니다.
0x0000000????
왜 내가 입력한 값을 1번을 눌러 인스턴스를 만드는 Man 주소가 내가 입력한 파일에 입력한 값이 아닌가....
엄청난 시간이 흘렀습니다.
두번 실행을 해보았습니다. 이번에도 3을 눌러 메모리를 비우고 2를 눌러 할당을 두번 연속으로 해봤습니다. 그랬더니 원래라면 0x00401570이 나와야하는 자리에 41414141 내가 파일에 입력한 값이 나오는 것을 확인할 수 있었습니다.
이제 됬습니다. 공격자가 마음대로 함수 호출 포인트를 조절할 수 있게 되었습니다.
혹시몰라 몇번더 진행해 봤는데 내가 할당한 만큼 메모리에 할당이 되네요!!!
그럼 공격할 수 있게 되었습니다.
401570보다 8적은 401568을 파일에 써주어 introduce() 대신에 give_shell() 를 호출 할 수 있게 되었습니다.
제가 풀어본 pwnable중에서는 어려운 편이였던거 같아요....
저도 더 공부해야겠습니다.
0 개의 댓글