분석 환경
OS
• Linux Kernel 3.8
개요
Dirty COW 라고도 불리는 CVE-2016-5195 취약점은
리눅스 커널 메모리 서브시스템에서 read-only memory에 write를 할때 copy-on-write가 발생한다. 이때 race-condition을 이용해 read-only 영역 메모리에 쓰기를하여 권한상승을 할 수 있다.
배경지식
Copy on Write
위 사진은 process1이 fork()를 통해 자식프로세스인 process2를 생성한 모습이다.
만약 자식프로세스가 page C를 사용중에 있고, 이때 부모프로세스가 page C의 데이터를 수정해야 하는 상황이 된다면 자식 프로세스가 사용 중이므로 직접 변경이 불가능하다.
그러면 OS에서는 Page C를 복사한 다음 거기에 수정을 하는 방법을 Copy on Write라고 한다.
분석
DirtyCow에서 말하는 copy-on-write의 맥락은 Read-only 파일이 매핑된 메모리에 무언가를 쓰려고할때
물리 메모리에 존재하는 map의 사본을 만들어서 거기에 write를 한다는 것을 말한다.
위 그림처럼 메모리 특정 위치에 대한 쓰기 권한을 가지고 있지만, 그 메모리가 Read-Only로 매핑되어있다고 가정해보자.
커널은 R/O영역인 것을 파악하고 Private-copy 페이지를 를 생성한다. 즉, 사본을 생성한다는 것이다.
이후 addr:0x12345678에 대한 포인터를 사본페이지를 가리키게 되고, 실질적으로 write를 했을때 원본에 write가 되는것이 아닌 private copy에 write가 될 것이다.
이렇게되면 UserProcess입장에서는 정상적으로 write를 할 수 있을 것이고, disk입장에서는 변조가 일어나지 않아 R/O권한이 지켜졌다고 볼 수 있다.
Read-only 메모리에 어떻게 쓰기 요청이 가능한지 궁금할 수 있다. 정상적인 방법은 아니지만 /proc/ 폴더를 이용해서 Read-Only memory에 write를 시도할 수 있다. 일단 계속해서 보자.
리눅스의 /proc/[PID]/폴더안에 해당 프로세스의 정보들을 볼 수 있는 파일이 있다. 여기서 dirtcow와 연관되는 파일은 /proc/self/mem파일이다.
sef는 자신의 pid로 된 링크이다. mem은 해당 프로세스의 virtual memory를 대변한다. 당연하게 자신의 메모리이니깐 write권한을 가지고 있어야하므로 rw-권한을 가지고 있다.
위 그림과 같이 가상메모리를 열어서 offset으로 Read-Only 메모리에 접근을 할 수 있다.
그러면 커널에서는 Copy on Write가 일어나서 실제 디스크에 write가 아닌 private copy page를 만들어 거기에 대한 쓰기가 일어난다.
여기까지 진행했을때는 위협이 일어나지 않는다.
추가로 알아야할 함수가 있다.
int madvise(void* addr, size_t length, int advice);
adivce에 MADV_DONTNEED플래그를 넣어주면 해당 페이지(addr)을 사용하지 않는다는 뜻이다.
이제 커널 디버깅과 소스코드 분석을 하면서 어떻게 취약점이 발생하는지 보겠다.
write()를 통해 데이터를 특정 메모리에 쓰게되면 아래와 같은 호출이 일어나게 된다.
write()->sys_write()->vfs_write()->mem_write()->mem_rw()->access_remote_vm() ->__get_user_pages()
여기서 취약점이 발생하는 부분은 __access_remote_vm(), __get_user_pages()이고, 이 함수들을 살펴보겠다.
get_user_pages()는 virtual address 범위를 커널 공간에 고정시키고 찾는 역할을 하는 함수이다.
함수를 실행하고 user page는 page 변수에 저장이 된다.
이후
page에 virtual addr이 담겨 있을 것이고, kmap()을 호출하여 물리 메모리로 바꿔준다. 그리고 copy_to_user_page()함수를 통해서 물리메모리에 buf를 write한다.
여기까지가 __access_remote_vm()의 흐름이다.
이제 __access_remote_vm()내부에서 호출되는 get_user_pages()함수에서 어떻게 Copy-On-Write가 발생하고 왜 취약해지는 살펴보자.
아래는 get_user_pages()코드 중 일부이다.
gup_flags or foll_flags는 사용자 메모리 페이지에 액세스하거나 원하는 이유와 방법에 대한 정보가 담겨있는 플래그로 보면된다.
( gup_flags는 __get_user_pages()호출 될때 인자로 전달된다.)
while loop에서 foll_flags를 만족하는 page를 찾기 위해서 follow_page()를 호출하고 찾지 못하면 바로 아래의 루틴을 실행한다.
(참고로 start는 write하려고 하는 주소이다.)
여기까지 간단하게 요약하자면,
현재 상황이 R/W권한이 있는 메모리에서 offset으로 Read-Only mapping page 까지 올린 다음 write를 하려고 하는 상황이다.
follow_page()의 인자로 start와 foll_flag가 들어가는데 이때 start는 Read-Only mapping page의 주소이고 해당 페이지에서 foll_flag가 있는 페이지를 찾고자 한다. 즉, R/O page에서 write권한이 있는 페이지를 찾고있는 것이다 follow_pages()를 통해서 말이다.
하지만 해당 페이지에 write권한이 없기때문에 page에는 NULL이 나올 것이다.
1787번 코드를 실행시키면서 write하려고 할떄 발생한 오류이기 때문에 fault_flags변수에 WRITE때문에 오류가 발생했다는 플래그를 추가한다.
이후 Page fault handler인 handle_mm_fault()함수를 통해서 page fault를 해결하게 된다.
이경우 우리가 write하고자 하는 memory mapping 공간이 read-olny이기 때문에 handle_mm_fault()는 원래 권한의 구성을 준수하면서 새로운 read-only page를 만들게된다. 이것이 COW page이다.
(handle_mm_fault() 내부적으로 do_wp_page()를 통해서 cow page를 생성한다.)
여기서 중요한 것이 있다.
현재까지 봤을때는 Copy-on-Write된 페이지는 orignal page와 똑같은 read-only권한으로 생성된다.
COW page는 write권한이 있어야하는거 아닌가 ????????? 여기까지 봤을때는 write권한이 없는 COW page에 어떻게 쓰기가 가능하다는 말인가...
쓰기가 가능한 이유는 계속해서 다음 코드를 분석해보면 알것이다.
Copy-On-Write가 발생하면(ret& VM_FAULT_WRITE == TRUE) foll_flags에서 write권한을 제거한다.
그리고 나서 다시 1786번째 코드로 돌아간 다음 write 권한이 없는 foll_flags를 follow_page()인자로 넣어 page를 seaching한다.
즉. read-only flag를 가지고 COW page를 찾는다. 이로서 COW page에 write가 가능해진다.
만약!
write 권한을 빼지않으면 loop로 돌아갔을때 또다시 write권한이 있는 page를 찾게 될 것이다. 즉. COW page도 못찾는다.
(COW page는 original page랑 동일한 권한을 가지므로)
foll_flags에서 write권한을 제거 -> follow_page()에서 foll_flag로 page searching -> return COW page
cow page make and search 흐름은 위와 같다.
만약 write 권한을 제거하고 follow_page()를 호출하기전에 cow page를 madvise()로 없애버리면???
없애버리면 write권한체크를 안하므로 orignal mapping page를 찾게된다. 그럼 거기에 write를 하면 취약점 발생!
이 상황을 race condition을 이용하면 된다.
__get_user_pages()에서 전달받은 flag를 그대로 foll_flags에 복사 해준 것을 볼 수 있다.
write권한을 가진 page를 찾기 시작하면 NULL이 나올 것이고 FAULT_FLAG_WRITE로 handle_mm_fault()으로 cow page를 만들게 된다.
write권한을 제거해줄때 fault_flag를 확인해보면 write fault값이 추가되어 있는 것을 확인할 수 있다.
follow_page()로 r/o 페이지를 탐색하고 COW page가 반환되는 것을 볼 수 있다.
follow_page()로 cow page를 탐색하기전에 madvise()로 cow page를 날려버리면 original mapping page를 반환하게 될 것이고 트리거 성공한다.
이제 exploit해보면 아래와 같은 결과를 볼 수 있다.
환경구축..
Ubuntu 16.04에서 진행하였고, gcc 버젼을 4.9로 다운그레이드 시킨후
https://cdn.kernel.org/pub/linux/kernel/v3.x/ 에서 커널 3.8을 다운로드해 컴파일을 진행한다.
rootfs이미지는 'debootstrap rootfs 이미지 생성' 관련해서 찾아보면 된다.
참조
https://manpd.tistory.com/230
https://marcokhan.tistory.com/243
https://cdn.kernel.org/pub/linux/kernel/v3.x/
https://elixir.bootlin.com/linux/latest/ident/__access_remote_vm
https://chao-tic.github.io/blog/2017/05/24/dirty-cow
https://www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/9_VirtualMemory.html
'ETC' 카테고리의 다른 글
WINAPI 정리 (0) | 2022.05.11 |
---|---|
Code execution with de-serialization (leads to UAC bypass) (0) | 2022.05.09 |
CVE-2021-1732 LPE vulnerability analysis (2) | 2022.04.08 |
LLVM 난독화 (feat. 기법 3가지) (0) | 2022.01.26 |
COM(Component Object Model)의 개념 잡기 (0) | 2021.11.21 |