ETC

[CVE-2016-5195] Dirty COW 분석

wsoh9812 2022. 5. 2. 08:33

분석 환경

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