#dokydoky

[Pwnerable.tw] Ghostparty(450pts) - source code audit & exploit 본문

System Hacking

[Pwnerable.tw] Ghostparty(450pts) - source code audit & exploit

dokydoky 2017. 5. 24. 13:30

ghostparty.cpp 소스를 보면서 실제 문제가 될만한 곳들을 먼 훑어본 후, 익스플로잇 해봅니다.

전체 소스코드는 여기서 확인.

https://github.com/dokydoky/writeup/blob/master/pwnerable.tw/ghostparty/ghostparty.cpp

Vulerability case 1

Uninitialized variable, UAF, memory leak
class Alan : public Ghost {
        public :
                Alan():lightsaber(NULL){
                        type = "Alan" ;
                };
 
                Alan(int ghostage,string ghostname,string ghostmsg){
                        type = "Alan";
                        age = ghostage ;
                        name = new char[ghostname.length() + 1];
                        strcpy(name,ghostname.c_str());
                        msg = ghostmsg ;
                        //lightsaber 초기화 없음
                };
 
                //복사생성자, 대입연산자 없음 -> UAF(need to check)
 
                void addlightsaber(string str){
                        lightsaber = (char*)str.c_str();
                        // string str은 이 새로 생성되고 함수가 끝나면 삭제된다. 문자열 저장된 heap도 마찬가지
                        // lightsaber은 이 함수가 끝나면 dangling pointer => UAF
                }
 
                void ghostinfo(){
                        cout << "Type : " << type << endl ;
                        cout << "Name : " << name << endl ;
                        cout << "Age : " << age << endl ;
                        cout << "Lightsaber : " << lightsaber << endl ;
                        // lightsaber 출력시 초기화가 안되있는 상황에 info leak 가능?
                        // => Alan객체 생성 후, 항상 addlightbar가 호출되고
                        //    addlightbar에서 lightsabar를 초기화하여, leak 불가
                }
                ~Alan(){
                  //lightsaber의 del이 없다.(need to check)
                };
        private :
                char *lightsaber ;
};
  
lightsaber의 UAF 취약점은 PIE를 leak할 수 있다.
lightsaber에 0x60을 할당하면 0x60크기의 dangling pointer가 생기고,
그 때 0x60크기의 객체를 할당하면 lightsaber가 객체의 vtable를 갖고 있다. => ghostinfo()가 호출될 때 vtable주소 leak
EIP control은 불가.. ~Alan()에서 lightsaber를 delete하면 추가적인 exploit이 가능하나 delete 하지 않음.
=> 이 취약점으로 못풀게 의도적으로 해놓은 듯.

 

Vulerability case 2

uninitialized variable, off-by-one
281 class Kasa : public Ghost {
282         public :
283                 Kasa(){
284                         type = "Kasa-obake" ;
285                 };
286
287                 Kasa(int ghostage,string ghostname,string ghostmsg){
288                         type = "Kasa-obake";
289                         age = ghostage ;
290                         name = new char[ghostname.length() + 1];
291                         strcpy(name,ghostname.c_str());
292                         msg = ghostmsg ;
293                         // uninitialized variable : foot => can be used for leakage vul? : No
294                         // 객체중에 Kasa랑 동일한 크기의 객체가 없고, 동일한 크기의 heap chunk를 만들 수 있는 방법이
295                         // char[], String뿐인데, foot이 int이기 때문에 입력된 값 그대로 출력되서 의미없음.
296                 };
297                
298                 void addinfo(int ghostfoot,string str){
299                         unsigned int size = 0 ;
300                         char buf[20] ;
301                         foot = ghostfoot ;
302                         eyes = str ;
303                         cout << "Input to echo :" ;
304                         size = read(0,buf,20);
305                         buf[size] = 0;
306                         // off-by-one : 1byte null  => need to check in binary
307                         // buf는 1byte null override가 있지만, 실제로 바이너리에서 32byte가 할당되서 별 문제 없음.
                               (size는 실제 스택에 할당안됨. 레지스터)
308                         cout << "echo :" << buf << endl ;
309                 };
310                
311                 void ghostinfo(){
312                         cout << "Type : " << type << endl ;
313                         cout << "Name : " << name << endl ;
314                         cout << "Age : " << age << endl ; 
315                         cout << "Foot : " << foot << endl ;
316                         cout << "Eyes : " << eyes << endl ;
317                 }
318                 ~Kasa(){
319                         foot = 0 ;
320                         eyes.clear();
321                 };
322         private :
323                 int foot ;
324                 string eyes ;
325 };
addinfo() off-by-one check in binary
// buf referred code
297                 void addinfo(int ghostfoot,string str){
298                         unsigned int size = 0 ;
299                         char buf[20] ;
300                         foot = ghostfoot ;
301                         eyes = str ;
302                         cout << "Input to echo :" ;
303                         size = read(0,buf,20);
304                         buf[size] = 0;
305                         // 입력값이 20byte 이상일 경우 1byte 초과하여 null overwrite.
307                         cout << "echo :" << buf << endl ;
308                 };
309
 
// stack offset in binary using IDA
-00000000000000A1                 db ? ; undefined
-00000000000000A0 buf             db 32 dup(?)  << buf[20] + padding(12)
-0000000000000080 randomBuf       db 32 dup(?)
-0000000000000060 secretBuf       db 32 dup(?)
-0000000000000040                 db ? ; undefined
 
buf는 1byte null override가 있지만, 실제로 바이너리에서 32byte가 할당되서 별 문제 없음.

 

Vulerability case 3

추가적으로 살펴보면 Zombie.addsecret에서 sfd를 close하지 않는데, exploit에 도움이 되지는 않음.

그리고!! (char *) 타입의 멤버를 가진 객체들 중에 복사생성자, 대입연산자를 정의하지 않아서 UAF취약점이 있는 객체들이 있음.
디폴트 복사생성자는 얕은 복사(shallow copy)를 하기 때문에 발생.

 

shallow copy constructor
 class Vampire : public Ghost {
    public :
        Vampire():blood(NULL){
            type = "Vampire" ;
        };
         
        Vampire(int ghostage,string ghostname,string ghostmsg){
            type = "Vampire";
            age = ghostage ;
            name = new char[ghostname.length() + 1];
            strcpy(name,ghostname.c_str());
            msg = ghostmsg ;
            blood = NULL ;
        };
 
        //복사생성자, 대입연산자 없음 -> UAF
 
        void addblood(string com){
            blood = new char[com.length()+1];
            memcpy(blood,com.c_str(),com.length());
        }
 
        void ghostinfo(){
            cout << "Type : " << type << endl ;
            cout << "Name : " << name << endl ;
            cout << "Age : " << age << endl ;
            cout << "Blood : " << blood << endl ;
        }
        ~Vampire(){
            delete[] blood;
        };
    private :
        char *blood ;
};
  
template <class T>
void speaking(T ghost){
    ghost.speak();
};


문제가 있는 class들은 아래와 같다.

    • Vampire, Alan : 복사생성자, 대입연산자 없음
    • Devil, Zombie : 대입연산자 없음 

하지만, 대입연산자로 인해 실제 코드에서 취약점이 발생되는 부분은 없고,
Alan은 위에서 설명했듯이 destructor에 delete이 없어서 PIE leak까지만 가능.
Vampire의 shallow copy constructor로 인한 UAF 취약점을 이용해 exploit 해봅시다.

Exploit code

https://github.com/dokydoky/writeup/blob/master/pwnerable.tw/ghostparty/ghostExploit.py

 

How to fix it
  class Vampire : public Ghost {
    public :
        Vampire():blood(NULL){
            type = "Vampire" ;
        };
         
        Vampire(int ghostage,string ghostname,string ghostmsg){
            type = "Vampire";
            age = ghostage ;
            name = new char[ghostname.length() + 1];
            strcpy(name,ghostname.c_str());
            msg = ghostmsg ;
            blood = NULL ;
        };
 
        // 디폴트 복사 생성자와 디폴트 대입 연산자가 생성되지 않도록 명시한다.
        Vampire(const Vampire&) = delete;
        void operator=(const Vampire&) = delete;
 
        void addblood(string com){
            blood = new char[com.length()+1];
            memcpy(blood,com.c_str(),com.length());
        }
 
        void ghostinfo(){
            cout << "Type : " << type << endl ;
            cout << "Name : " << name << endl ;
            cout << "Age : " << age << endl ;
            cout << "Blood : " << blood << endl ;
        }
        ~Vampire(){
            delete[] blood;
        };
    private :
        char *blood ;
};
  
template <class T>
void speaking(T &ghost){ //change call-by-value to call-by-reference
    ghost.speak();
};

speaking 함수에서 parameter type을 Reference로 넘겨주면(Call-by-Value가 아닌 Call-by-Reference) 복사 생성자가 호출되지 않으므로 문제가 없다.

하지만, 근본적으로 문제를 해결하기 위해서는 디폴트 복사 생성자와 디폴트 대입 생성자가 생성되지 않도록 명시해야 한다.

이렇게 되면 Call-by-value로 넘기는 경우 compile time에 error가 발생하므로, 취약점이 발생하는 코드를 방지할 수 있다.

참고 : https://www.securecoding.cert.org/confluence/display/cplusplus/MEM51-CPP.+Properly+deallocate+dynamically+allocated+resources 

 

추가로,

 vtable을 조작할 때 speak() 함수의 offset을 조작해서 exploit 할 수 없다.
출제자가 의도한 건지는 모르겠지만 아래 코드에서 speaking(**iter)가 호출될 때 type이 항상 Ghost이기 때문에 
바이너리에서 보면 speaking함수에서 vtable을 참조하지 않고 항상 Ghost.speak()이 호출된다.
그래서 ghostinfo()의 offset을 조작해서 exploit 해야 한다.

Misused templete function
template <class T>
void speaking(T ghost){
        ghost.speak();
};
 
void night(){
        unsigned int num ;
        vector<Ghost *>::iterator iter ;
        for(iter = ghostlist.begin();iter != ghostlist.end() ; iter++){
                num = 31+rand()%7;
                cout << "\033[" <<  num << "m";
                speaking(**iter);  // 항상 Ghost type의 객체를 넘김
                cout << "\033[0m";
        }
}

 

Comments